net-ldap 0.3.1 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/Contributors.rdoc +4 -0
  3. data/Hacking.rdoc +3 -8
  4. data/History.rdoc +181 -0
  5. data/README.rdoc +44 -12
  6. data/lib/net-ldap.rb +1 -1
  7. data/lib/net/ber.rb +41 -7
  8. data/lib/net/ber/ber_parser.rb +21 -7
  9. data/lib/net/ber/core_ext.rb +11 -18
  10. data/lib/net/ber/core_ext/array.rb +14 -0
  11. data/lib/net/ber/core_ext/integer.rb +74 -0
  12. data/lib/net/ber/core_ext/string.rb +24 -4
  13. data/lib/net/ber/core_ext/true_class.rb +2 -3
  14. data/lib/net/ldap.rb +441 -639
  15. data/lib/net/ldap/auth_adapter.rb +29 -0
  16. data/lib/net/ldap/auth_adapter/gss_spnego.rb +41 -0
  17. data/lib/net/ldap/auth_adapter/sasl.rb +62 -0
  18. data/lib/net/ldap/auth_adapter/simple.rb +34 -0
  19. data/lib/net/ldap/connection.rb +716 -0
  20. data/lib/net/ldap/dataset.rb +23 -9
  21. data/lib/net/ldap/dn.rb +13 -14
  22. data/lib/net/ldap/entry.rb +27 -9
  23. data/lib/net/ldap/error.rb +49 -0
  24. data/lib/net/ldap/filter.rb +58 -32
  25. data/lib/net/ldap/instrumentation.rb +23 -0
  26. data/lib/net/ldap/password.rb +23 -14
  27. data/lib/net/ldap/pdu.rb +70 -6
  28. data/lib/net/ldap/version.rb +5 -0
  29. data/lib/net/snmp.rb +237 -241
  30. metadata +71 -116
  31. data/.autotest +0 -11
  32. data/.gemtest +0 -0
  33. data/.rspec +0 -2
  34. data/Manifest.txt +0 -49
  35. data/Rakefile +0 -74
  36. data/autotest/discover.rb +0 -1
  37. data/lib/net/ber/core_ext/bignum.rb +0 -22
  38. data/lib/net/ber/core_ext/fixnum.rb +0 -66
  39. data/net-ldap.gemspec +0 -58
  40. data/spec/integration/ssl_ber_spec.rb +0 -36
  41. data/spec/spec.opts +0 -2
  42. data/spec/spec_helper.rb +0 -5
  43. data/spec/unit/ber/ber_spec.rb +0 -109
  44. data/spec/unit/ber/core_ext/string_spec.rb +0 -51
  45. data/spec/unit/ldap/dn_spec.rb +0 -80
  46. data/spec/unit/ldap/entry_spec.rb +0 -51
  47. data/spec/unit/ldap/filter_spec.rb +0 -84
  48. data/spec/unit/ldap_spec.rb +0 -48
  49. data/test/common.rb +0 -3
  50. data/test/test_entry.rb +0 -59
  51. data/test/test_filter.rb +0 -122
  52. data/test/test_ldap_connection.rb +0 -24
  53. data/test/test_ldif.rb +0 -79
  54. data/test/test_password.rb +0 -17
  55. data/test/test_rename.rb +0 -77
  56. data/test/test_snmp.rb +0 -114
  57. data/test/testdata.ldif +0 -101
  58. data/testserver/ldapserver.rb +0 -210
  59. data/testserver/testdata.ldif +0 -101
@@ -1,5 +1,5 @@
1
1
  # -*- ruby encoding: utf-8 -*-
2
- require 'net/ber/ber_parser'
2
+ require_relative 'ber_parser'
3
3
  # :stopdoc:
4
4
  class IO
5
5
  include Net::BER::BERParser
@@ -19,44 +19,37 @@ end
19
19
  module Net::BER::Extensions # :nodoc:
20
20
  end
21
21
 
22
- require 'net/ber/core_ext/string'
22
+ require_relative 'core_ext/string'
23
23
  # :stopdoc:
24
24
  class String
25
25
  include Net::BER::BERParser
26
26
  include Net::BER::Extensions::String
27
27
  end
28
28
 
29
- require 'net/ber/core_ext/array'
29
+ require_relative 'core_ext/array'
30
30
  # :stopdoc:
31
- class Array
31
+ class Array
32
32
  include Net::BER::Extensions::Array
33
33
  end
34
34
  # :startdoc:
35
35
 
36
- require 'net/ber/core_ext/bignum'
36
+ require_relative 'core_ext/integer'
37
37
  # :stopdoc:
38
- class Bignum
39
- include Net::BER::Extensions::Bignum
38
+ class Integer
39
+ include Net::BER::Extensions::Integer
40
40
  end
41
41
  # :startdoc:
42
42
 
43
- require 'net/ber/core_ext/fixnum'
43
+ require_relative 'core_ext/true_class'
44
44
  # :stopdoc:
45
- class Fixnum
46
- include Net::BER::Extensions::Fixnum
47
- end
48
- # :startdoc:
49
-
50
- require 'net/ber/core_ext/true_class'
51
- # :stopdoc:
52
- class TrueClass
45
+ class TrueClass
53
46
  include Net::BER::Extensions::TrueClass
54
47
  end
55
48
  # :startdoc:
56
49
 
57
- require 'net/ber/core_ext/false_class'
50
+ require_relative 'core_ext/false_class'
58
51
  # :stopdoc:
59
- class FalseClass
52
+ class FalseClass
60
53
  include Net::BER::Extensions::FalseClass
61
54
  end
62
55
  # :startdoc:
@@ -79,4 +79,18 @@ module Net::BER::Extensions::Array
79
79
  oid = ary.pack("w*")
80
80
  [6, oid.length].pack("CC") + oid
81
81
  end
82
+
83
+ ##
84
+ # Converts an array into a set of ber control codes
85
+ # The expected format is [[control_oid, criticality, control_value(optional)]]
86
+ # [['1.2.840.113556.1.4.805',true]]
87
+ #
88
+ def to_ber_control
89
+ #if our array does not contain at least one array then wrap it in an array before going forward
90
+ ary = self[0].kind_of?(Array) ? self : [self]
91
+ ary = ary.collect do |control_sequence|
92
+ control_sequence.collect(&:to_ber).to_ber_sequence.reject_empty_ber_arrays
93
+ end
94
+ ary.to_ber_sequence.reject_empty_ber_arrays
95
+ end
82
96
  end
@@ -0,0 +1,74 @@
1
+ # -*- ruby encoding: utf-8 -*-
2
+ ##
3
+ # BER extensions to the Integer class, affecting Fixnum and Bignum objects.
4
+ module Net::BER::Extensions::Integer
5
+ ##
6
+ # Converts the Integer to BER format.
7
+ def to_ber
8
+ "\002#{to_ber_internal}"
9
+ end
10
+
11
+ ##
12
+ # Converts the Integer to BER enumerated format.
13
+ def to_ber_enumerated
14
+ "\012#{to_ber_internal}"
15
+ end
16
+
17
+ ##
18
+ # Converts the Integer to BER length encoding format.
19
+ def to_ber_length_encoding
20
+ if self <= 127
21
+ [self].pack('C')
22
+ else
23
+ i = [self].pack('N').sub(/^[\0]+/, "")
24
+ [0x80 + i.length].pack('C') + i
25
+ end
26
+ end
27
+
28
+ ##
29
+ # Generate a BER-encoding for an application-defined INTEGER. Examples of
30
+ # such integers are SNMP's Counter, Gauge, and TimeTick types.
31
+ def to_ber_application(tag)
32
+ [0x40 + tag].pack("C") + to_ber_internal
33
+ end
34
+
35
+ ##
36
+ # Used to BER-encode the length and content bytes of an Integer. Callers
37
+ # must prepend the tag byte for the contained value.
38
+ def to_ber_internal
39
+ # Compute the byte length, accounting for negative values requiring two's
40
+ # complement.
41
+ size = 1
42
+ size += 1 until (((self < 0) ? ~self : self) >> (size * 8)).zero?
43
+
44
+ # Padding for positive, negative values. See section 8.5 of ITU-T X.690:
45
+ # http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf
46
+
47
+ # For positive integers, if most significant bit in an octet is set to one,
48
+ # pad the result (otherwise it's decoded as a negative value).
49
+ if self > 0 && (self & (0x80 << (size - 1) * 8)) > 0
50
+ size += 1
51
+ end
52
+
53
+ # And for negative integers, pad if the most significant bit in the octet
54
+ # is not set to one (othwerise, it's decoded as positive value).
55
+ if self < 0 && (self & (0x80 << (size - 1) * 8)) == 0
56
+ size += 1
57
+ end
58
+
59
+ # Store the size of the Integer in the result
60
+ result = [size]
61
+
62
+ # Appends bytes to result, starting with higher orders first. Extraction
63
+ # of bytes is done by right shifting the original Integer by an amount
64
+ # and then masking that with 0xff.
65
+ while size > 0
66
+ # right shift size - 1 bytes, mask with 0xff
67
+ result << ((self >> ((size - 1) * 8)) & 0xff)
68
+ size -= 1
69
+ end
70
+
71
+ result.pack('C*')
72
+ end
73
+ private :to_ber_internal
74
+ end
@@ -16,11 +16,27 @@ module Net::BER::Extensions::String
16
16
  [code].pack('C') + raw_string.length.to_ber_length_encoding + raw_string
17
17
  end
18
18
 
19
+ ##
20
+ # Converts a string to a BER string but does *not* encode to UTF-8 first.
21
+ # This is required for proper representation of binary data for Microsoft
22
+ # Active Directory
23
+ def to_ber_bin(code = 0x04)
24
+ [code].pack('C') + length.to_ber_length_encoding + self
25
+ end
26
+
19
27
  def raw_utf8_encoded
20
28
  if self.respond_to?(:encode)
21
29
  # Strings should be UTF-8 encoded according to LDAP.
22
30
  # However, the BER code is not necessarily valid UTF-8
23
- self.encode('UTF-8').force_encoding('ASCII-8BIT')
31
+ begin
32
+ self.encode('UTF-8').force_encoding('ASCII-8BIT')
33
+ rescue Encoding::UndefinedConversionError
34
+ self
35
+ rescue Encoding::ConverterNotFoundError
36
+ self
37
+ rescue Encoding::InvalidByteSequenceError
38
+ self
39
+ end
24
40
  else
25
41
  self
26
42
  end
@@ -46,15 +62,19 @@ module Net::BER::Extensions::String
46
62
  def read_ber(syntax = nil)
47
63
  StringIO.new(self).read_ber(syntax)
48
64
  end
49
-
65
+
50
66
  ##
51
- # Destructively reads a BER object from the string.
67
+ # Destructively reads a BER object from the string.
52
68
  def read_ber!(syntax = nil)
53
69
  io = StringIO.new(self)
54
70
 
55
71
  result = io.read_ber(syntax)
56
72
  self.slice!(0...io.pos)
57
-
73
+
58
74
  return result
59
75
  end
76
+
77
+ def reject_empty_ber_arrays
78
+ self.gsub(/0\000/n, '')
79
+ end
60
80
  end
@@ -5,8 +5,7 @@ module Net::BER::Extensions::TrueClass
5
5
  ##
6
6
  # Converts +true+ to the BER wireline representation of +true+.
7
7
  def to_ber
8
- # 20100319 AZ: Note that this may not be the completely correct value,
9
- # per some test documentation. We need to determine the truth of this.
10
- "\001\001\001"
8
+ # http://tools.ietf.org/html/rfc4511#section-5.1
9
+ "\001\001\xFF".force_encoding("ASCII-8BIT")
11
10
  end
12
11
  end
data/lib/net/ldap.rb CHANGED
@@ -17,12 +17,22 @@ module Net # :nodoc:
17
17
  end
18
18
  require 'socket'
19
19
 
20
- require 'net/ber'
21
- require 'net/ldap/pdu'
22
- require 'net/ldap/filter'
23
- require 'net/ldap/dataset'
24
- require 'net/ldap/password'
25
- require 'net/ldap/entry'
20
+ require_relative 'ber'
21
+ require_relative 'ldap/pdu'
22
+ require_relative 'ldap/filter'
23
+ require_relative 'ldap/dataset'
24
+ require_relative 'ldap/password'
25
+ require_relative 'ldap/entry'
26
+ require_relative 'ldap/instrumentation'
27
+ require_relative 'ldap/connection'
28
+ require_relative 'ldap/version'
29
+ require_relative 'ldap/error'
30
+ require_relative 'ldap/auth_adapter'
31
+ require_relative 'ldap/auth_adapter/simple'
32
+ require_relative 'ldap/auth_adapter/sasl'
33
+
34
+ Net::LDAP::AuthAdapter.register([:simple, :anon, :anonymous], Net::LDAP::AuthAdapter::Simple)
35
+ Net::LDAP::AuthAdapter.register(:sasl, Net::LDAP::AuthAdapter::Sasl)
26
36
 
27
37
  # == Quick-start for the Impatient
28
38
  # === Quick Example of a user-authentication against an LDAP directory:
@@ -69,6 +79,14 @@ require 'net/ldap/entry'
69
79
  #
70
80
  # p ldap.get_operation_result
71
81
  #
82
+ # === Setting connect timeout
83
+ #
84
+ # By default, Net::LDAP uses TCP sockets with a connection timeout of 5 seconds.
85
+ #
86
+ # This value can be tweaked passing the :connect_timeout parameter.
87
+ # i.e.
88
+ # ldap = Net::LDAP.new ...,
89
+ # :connect_timeout => 3
72
90
  #
73
91
  # == A Brief Introduction to LDAP
74
92
  #
@@ -241,15 +259,19 @@ require 'net/ldap/entry'
241
259
  # and then keeps it open while it executes a user-supplied block.
242
260
  # Net::LDAP#open closes the connection on completion of the block.
243
261
  class Net::LDAP
244
- VERSION = "0.3.1"
245
-
246
- class LdapError < StandardError; end
262
+ include Net::LDAP::Instrumentation
247
263
 
248
264
  SearchScope_BaseObject = 0
249
265
  SearchScope_SingleLevel = 1
250
266
  SearchScope_WholeSubtree = 2
251
- SearchScopes = [ SearchScope_BaseObject, SearchScope_SingleLevel,
252
- SearchScope_WholeSubtree ]
267
+ SearchScopes = [SearchScope_BaseObject, SearchScope_SingleLevel,
268
+ SearchScope_WholeSubtree]
269
+
270
+ DerefAliases_Never = 0
271
+ DerefAliases_Search = 1
272
+ DerefAliases_Find = 2
273
+ DerefAliases_Always = 3
274
+ DerefAliasesArray = [DerefAliases_Never, DerefAliases_Search, DerefAliases_Find, DerefAliases_Always]
253
275
 
254
276
  primitive = { 2 => :null } # UnbindRequest body
255
277
  constructed = {
@@ -301,42 +323,129 @@ class Net::LDAP
301
323
  :constructed => constructed,
302
324
  }
303
325
 
326
+ universal = {
327
+ constructed: {
328
+ 107 => :array, #ExtendedResponse (PasswdModifyResponseValue)
329
+ },
330
+ }
331
+
304
332
  AsnSyntax = Net::BER.compile_syntax(:application => application,
333
+ :universal => universal,
305
334
  :context_specific => context_specific)
306
335
 
307
336
  DefaultHost = "127.0.0.1"
308
337
  DefaultPort = 389
309
338
  DefaultAuth = { :method => :anonymous }
310
339
  DefaultTreebase = "dc=com"
311
-
312
- StartTlsOid = "1.3.6.1.4.1.1466.20037"
313
-
340
+ DefaultForceNoPage = false
341
+
342
+ StartTlsOid = '1.3.6.1.4.1.1466.20037'
343
+ PasswdModifyOid = '1.3.6.1.4.1.4203.1.11.1'
344
+
345
+ # https://tools.ietf.org/html/rfc4511#section-4.1.9
346
+ # https://tools.ietf.org/html/rfc4511#appendix-A
347
+ ResultCodeSuccess = 0
348
+ ResultCodeOperationsError = 1
349
+ ResultCodeProtocolError = 2
350
+ ResultCodeTimeLimitExceeded = 3
351
+ ResultCodeSizeLimitExceeded = 4
352
+ ResultCodeCompareFalse = 5
353
+ ResultCodeCompareTrue = 6
354
+ ResultCodeAuthMethodNotSupported = 7
355
+ ResultCodeStrongerAuthRequired = 8
356
+ ResultCodeReferral = 10
357
+ ResultCodeAdminLimitExceeded = 11
358
+ ResultCodeUnavailableCriticalExtension = 12
359
+ ResultCodeConfidentialityRequired = 13
360
+ ResultCodeSaslBindInProgress = 14
361
+ ResultCodeNoSuchAttribute = 16
362
+ ResultCodeUndefinedAttributeType = 17
363
+ ResultCodeInappropriateMatching = 18
364
+ ResultCodeConstraintViolation = 19
365
+ ResultCodeAttributeOrValueExists = 20
366
+ ResultCodeInvalidAttributeSyntax = 21
367
+ ResultCodeNoSuchObject = 32
368
+ ResultCodeAliasProblem = 33
369
+ ResultCodeInvalidDNSyntax = 34
370
+ ResultCodeAliasDereferencingProblem = 36
371
+ ResultCodeInappropriateAuthentication = 48
372
+ ResultCodeInvalidCredentials = 49
373
+ ResultCodeInsufficientAccessRights = 50
374
+ ResultCodeBusy = 51
375
+ ResultCodeUnavailable = 52
376
+ ResultCodeUnwillingToPerform = 53
377
+ ResultCodeNamingViolation = 64
378
+ ResultCodeObjectClassViolation = 65
379
+ ResultCodeNotAllowedOnNonLeaf = 66
380
+ ResultCodeNotAllowedOnRDN = 67
381
+ ResultCodeEntryAlreadyExists = 68
382
+ ResultCodeObjectClassModsProhibited = 69
383
+ ResultCodeAffectsMultipleDSAs = 71
384
+ ResultCodeOther = 80
385
+
386
+ # https://tools.ietf.org/html/rfc4511#appendix-A.1
387
+ ResultCodesNonError = [
388
+ ResultCodeSuccess,
389
+ ResultCodeCompareFalse,
390
+ ResultCodeCompareTrue,
391
+ ResultCodeReferral,
392
+ ResultCodeSaslBindInProgress,
393
+ ]
394
+
395
+ # nonstandard list of "successful" result codes for searches
396
+ ResultCodesSearchSuccess = [
397
+ ResultCodeSuccess,
398
+ ResultCodeTimeLimitExceeded,
399
+ ResultCodeSizeLimitExceeded,
400
+ ]
401
+
402
+ # map of result code to human message
314
403
  ResultStrings = {
315
- 0 => "Success",
316
- 1 => "Operations Error",
317
- 2 => "Protocol Error",
318
- 3 => "Time Limit Exceeded",
319
- 4 => "Size Limit Exceeded",
320
- 10 => "Referral",
321
- 12 => "Unavailable crtical extension",
322
- 14 => "saslBindInProgress",
323
- 16 => "No Such Attribute",
324
- 17 => "Undefined Attribute Type",
325
- 20 => "Attribute or Value Exists",
326
- 32 => "No Such Object",
327
- 34 => "Invalid DN Syntax",
328
- 48 => "Inappropriate Authentication",
329
- 49 => "Invalid Credentials",
330
- 50 => "Insufficient Access Rights",
331
- 51 => "Busy",
332
- 52 => "Unavailable",
333
- 53 => "Unwilling to perform",
334
- 65 => "Object Class Violation",
335
- 68 => "Entry Already Exists"
404
+ ResultCodeSuccess => "Success",
405
+ ResultCodeOperationsError => "Operations Error",
406
+ ResultCodeProtocolError => "Protocol Error",
407
+ ResultCodeTimeLimitExceeded => "Time Limit Exceeded",
408
+ ResultCodeSizeLimitExceeded => "Size Limit Exceeded",
409
+ ResultCodeCompareFalse => "False Comparison",
410
+ ResultCodeCompareTrue => "True Comparison",
411
+ ResultCodeAuthMethodNotSupported => "Auth Method Not Supported",
412
+ ResultCodeStrongerAuthRequired => "Stronger Auth Needed",
413
+ ResultCodeReferral => "Referral",
414
+ ResultCodeAdminLimitExceeded => "Admin Limit Exceeded",
415
+ ResultCodeUnavailableCriticalExtension => "Unavailable crtical extension",
416
+ ResultCodeConfidentialityRequired => "Confidentiality Required",
417
+ ResultCodeSaslBindInProgress => "saslBindInProgress",
418
+ ResultCodeNoSuchAttribute => "No Such Attribute",
419
+ ResultCodeUndefinedAttributeType => "Undefined Attribute Type",
420
+ ResultCodeInappropriateMatching => "Inappropriate Matching",
421
+ ResultCodeConstraintViolation => "Constraint Violation",
422
+ ResultCodeAttributeOrValueExists => "Attribute or Value Exists",
423
+ ResultCodeInvalidAttributeSyntax => "Invalide Attribute Syntax",
424
+ ResultCodeNoSuchObject => "No Such Object",
425
+ ResultCodeAliasProblem => "Alias Problem",
426
+ ResultCodeInvalidDNSyntax => "Invalid DN Syntax",
427
+ ResultCodeAliasDereferencingProblem => "Alias Dereferencing Problem",
428
+ ResultCodeInappropriateAuthentication => "Inappropriate Authentication",
429
+ ResultCodeInvalidCredentials => "Invalid Credentials",
430
+ ResultCodeInsufficientAccessRights => "Insufficient Access Rights",
431
+ ResultCodeBusy => "Busy",
432
+ ResultCodeUnavailable => "Unavailable",
433
+ ResultCodeUnwillingToPerform => "Unwilling to perform",
434
+ ResultCodeNamingViolation => "Naming Violation",
435
+ ResultCodeObjectClassViolation => "Object Class Violation",
436
+ ResultCodeNotAllowedOnNonLeaf => "Not Allowed On Non-Leaf",
437
+ ResultCodeNotAllowedOnRDN => "Not Allowed On RDN",
438
+ ResultCodeEntryAlreadyExists => "Entry Already Exists",
439
+ ResultCodeObjectClassModsProhibited => "ObjectClass Modifications Prohibited",
440
+ ResultCodeAffectsMultipleDSAs => "Affects Multiple DSAs",
441
+ ResultCodeOther => "Other",
336
442
  }
337
443
 
338
- module LdapControls
339
- PagedResults = "1.2.840.113556.1.4.319" # Microsoft evil from RFC 2696
444
+ module LDAPControls
445
+ PAGED_RESULTS = "1.2.840.113556.1.4.319" # Microsoft evil from RFC 2696
446
+ SORT_REQUEST = "1.2.840.113556.1.4.473"
447
+ SORT_RESPONSE = "1.2.840.113556.1.4.474"
448
+ DELETE_TREE = "1.2.840.113556.1.4.805"
340
449
  end
341
450
 
342
451
  def self.result2string(code) #:nodoc:
@@ -345,6 +454,7 @@ class Net::LDAP
345
454
 
346
455
  attr_accessor :host
347
456
  attr_accessor :port
457
+ attr_accessor :hosts
348
458
  attr_accessor :base
349
459
 
350
460
  # Instantiate an object of type Net::LDAP to perform directory operations.
@@ -353,6 +463,8 @@ class Net::LDAP
353
463
  # described below. The following arguments are supported:
354
464
  # * :host => the LDAP server's IP-address (default 127.0.0.1)
355
465
  # * :port => the LDAP server's TCP port (default 389)
466
+ # * :hosts => an enumerable of pairs of hosts and corresponding ports with
467
+ # which to attempt opening connections (default [[host, port]])
356
468
  # * :auth => a Hash containing authorization parameters. Currently
357
469
  # supported values include: {:method => :anonymous} and {:method =>
358
470
  # :simple, :username => your_user_name, :password => your_password }
@@ -364,28 +476,90 @@ class Net::LDAP
364
476
  # specify a treebase. If you give a treebase value in any particular
365
477
  # call to #search, that value will override any treebase value you give
366
478
  # here.
479
+ # * :force_no_page => Set to true to prevent paged results even if your
480
+ # server says it supports them. This is a fix for MS Active Directory
481
+ # * :instrumentation_service => An object responsible for instrumenting
482
+ # operations, compatible with ActiveSupport::Notifications' public API.
367
483
  # * :encryption => specifies the encryption to be used in communicating
368
- # with the LDAP server. The value is either a Hash containing additional
369
- # parameters, or the Symbol :simple_tls, which is equivalent to
370
- # specifying the Hash {:method => :simple_tls}. There is a fairly large
371
- # range of potential values that may be given for this parameter. See
372
- # #encryption for details.
484
+ # with the LDAP server. The value must be a Hash containing additional
485
+ # parameters, which consists of two keys:
486
+ # method: - :simple_tls or :start_tls
487
+ # tls_options: - Hash of options for that method
488
+ # The :simple_tls encryption method encrypts <i>all</i> communications
489
+ # with the LDAP server. It completely establishes SSL/TLS encryption with
490
+ # the LDAP server before any LDAP-protocol data is exchanged. There is no
491
+ # plaintext negotiation and no special encryption-request controls are
492
+ # sent to the server. <i>The :simple_tls option is the simplest, easiest
493
+ # way to encrypt communications between Net::LDAP and LDAP servers.</i>
494
+ # If you get communications or protocol errors when using this option,
495
+ # check with your LDAP server administrator. Pay particular attention
496
+ # to the TCP port you are connecting to. It's impossible for an LDAP
497
+ # server to support plaintext LDAP communications and <i>simple TLS</i>
498
+ # connections on the same port. The standard TCP port for unencrypted
499
+ # LDAP connections is 389, but the standard port for simple-TLS
500
+ # encrypted connections is 636. Be sure you are using the correct port.
501
+ # The :start_tls like the :simple_tls encryption method also encrypts all
502
+ # communcations with the LDAP server. With the exception that it operates
503
+ # over the standard TCP port.
504
+ #
505
+ # To validate the LDAP server's certificate (a security must if you're
506
+ # talking over the public internet), you need to set :tls_options
507
+ # something like this...
508
+ #
509
+ # Net::LDAP.new(
510
+ # # ... set host, bind dn, etc ...
511
+ # encryption: {
512
+ # method: :simple_tls,
513
+ # tls_options: OpenSSL::SSL::SSLContext::DEFAULT_PARAMS,
514
+ # }
515
+ # )
516
+ #
517
+ # The above will use the operating system-provided store of CA
518
+ # certificates to validate your LDAP server's cert.
519
+ # If cert validation fails, it'll happen during the #bind
520
+ # whenever you first try to open a connection to the server.
521
+ # Those methods will throw Net::LDAP::ConnectionError with
522
+ # a message about certificate verify failing. If your
523
+ # LDAP server's certificate is signed by DigiCert, Comodo, etc.,
524
+ # you're probably good. If you've got a self-signed cert but it's
525
+ # been added to the host's OS-maintained CA store (e.g. on Debian
526
+ # add foobar.crt to /usr/local/share/ca-certificates/ and run
527
+ # `update-ca-certificates`), then the cert should pass validation.
528
+ # To ignore the OS's CA store, put your CA in a PEM-encoded file and...
529
+ #
530
+ # encryption: {
531
+ # method: :simple_tls,
532
+ # tls_options: { ca_file: '/path/to/my-little-ca.pem',
533
+ # ssl_version: 'TLSv1_1' },
534
+ # }
535
+ #
536
+ # As you might guess, the above example also fails the connection
537
+ # if the client can't negotiate TLS v1.1.
538
+ # tls_options is ultimately passed to OpenSSL::SSL::SSLContext#set_params
539
+ # For more details, see
540
+ # http://ruby-doc.org/stdlib-2.0.0/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html
373
541
  #
374
542
  # Instantiating a Net::LDAP object does <i>not</i> result in network
375
543
  # traffic to the LDAP server. It simply stores the connection and binding
376
- # parameters in the object.
544
+ # parameters in the object. That's why Net::LDAP.new doesn't throw
545
+ # cert validation errors itself; #bind does instead.
377
546
  def initialize(args = {})
378
547
  @host = args[:host] || DefaultHost
379
548
  @port = args[:port] || DefaultPort
549
+ @hosts = args[:hosts]
380
550
  @verbose = false # Make this configurable with a switch on the class.
381
551
  @auth = args[:auth] || DefaultAuth
382
552
  @base = args[:base] || DefaultTreebase
383
- encryption args[:encryption] # may be nil
553
+ @force_no_page = args[:force_no_page] || DefaultForceNoPage
554
+ @encryption = normalize_encryption(args[:encryption]) # may be nil
555
+ @connect_timeout = args[:connect_timeout]
384
556
 
385
557
  if pr = @auth[:password] and pr.respond_to?(:call)
386
558
  @auth[:password] = pr.call
387
559
  end
388
560
 
561
+ @instrumentation_service = args[:instrumentation_service]
562
+
389
563
  # This variable is only set when we are created with LDAP::open. All of
390
564
  # our internal methods will connect using it, or else they will create
391
565
  # their own.
@@ -429,7 +603,7 @@ class Net::LDAP
429
603
  @auth = {
430
604
  :method => :simple,
431
605
  :username => username,
432
- :password => password
606
+ :password => password,
433
607
  }
434
608
  end
435
609
  alias_method :auth, :authenticate
@@ -442,38 +616,12 @@ class Net::LDAP
442
616
  # additional capabilities are added, more configuration values will be
443
617
  # added here.
444
618
  #
445
- # Currently, the only supported argument is { :method => :simple_tls }.
446
- # (Equivalently, you may pass the symbol :simple_tls all by itself,
447
- # without enclosing it in a Hash.)
448
- #
449
- # The :simple_tls encryption method encrypts <i>all</i> communications
450
- # with the LDAP server. It completely establishes SSL/TLS encryption with
451
- # the LDAP server before any LDAP-protocol data is exchanged. There is no
452
- # plaintext negotiation and no special encryption-request controls are
453
- # sent to the server. <i>The :simple_tls option is the simplest, easiest
454
- # way to encrypt communications between Net::LDAP and LDAP servers.</i>
455
- # It's intended for cases where you have an implicit level of trust in the
456
- # authenticity of the LDAP server. No validation of the LDAP server's SSL
457
- # certificate is performed. This means that :simple_tls will not produce
458
- # errors if the LDAP server's encryption certificate is not signed by a
459
- # well-known Certification Authority. If you get communications or
460
- # protocol errors when using this option, check with your LDAP server
461
- # administrator. Pay particular attention to the TCP port you are
462
- # connecting to. It's impossible for an LDAP server to support plaintext
463
- # LDAP communications and <i>simple TLS</i> connections on the same port.
464
- # The standard TCP port for unencrypted LDAP connections is 389, but the
465
- # standard port for simple-TLS encrypted connections is 636. Be sure you
466
- # are using the correct port.
467
- #
468
- # <i>[Note: a future version of Net::LDAP will support the STARTTLS LDAP
469
- # control, which will enable encrypted communications on the same TCP port
470
- # used for unencrypted connections.]</i>
619
+ # This method is deprecated.
620
+ #
471
621
  def encryption(args)
472
- case args
473
- when :simple_tls, :start_tls
474
- args = { :method => args }
475
- end
476
- @encryption = args
622
+ warn "Deprecation warning: please give :encryption option as a Hash to Net::LDAP.new"
623
+ return if args.nil?
624
+ @encryption = normalize_encryption(args)
477
625
  end
478
626
 
479
627
  # #open takes the same parameters as #new. #open makes a network
@@ -516,17 +664,22 @@ class Net::LDAP
516
664
  # response codes instead of a simple numeric code.
517
665
  #++
518
666
  def get_operation_result
667
+ result = @result
519
668
  os = OpenStruct.new
520
- if @result.is_a?(Hash)
669
+ if result.is_a?(Net::LDAP::PDU)
670
+ os.extended_response = result.extended_response
671
+ result = result.result
672
+ end
673
+ if result.is_a?(Hash)
521
674
  # We might get a hash of LDAP response codes instead of a simple
522
675
  # numeric code.
523
- os.code = (@result[:resultCode] || "").to_i
524
- os.error_message = @result[:errorMessage]
525
- os.matched_dn = @result[:matchedDN]
526
- elsif @result
527
- os.code = @result
676
+ os.code = (result[:resultCode] || "").to_i
677
+ os.error_message = result[:errorMessage]
678
+ os.matched_dn = result[:matchedDN]
679
+ elsif result
680
+ os.code = result
528
681
  else
529
- os.code = 0
682
+ os.code = Net::LDAP::ResultCodeSuccess
530
683
  end
531
684
  os.message = Net::LDAP.result2string(os.code)
532
685
  os
@@ -553,18 +706,18 @@ class Net::LDAP
553
706
  # anything with the bind results. We then pass self to the caller's
554
707
  # block, where he will execute his LDAP operations. Of course they will
555
708
  # all generate auth failures if the bind was unsuccessful.
556
- raise Net::LDAP::LdapError, "Open already in progress" if @open_connection
709
+ raise Net::LDAP::AlreadyOpenedError, "Open already in progress" if @open_connection
557
710
 
558
- begin
559
- @open_connection = Net::LDAP::Connection.new(:host => @host,
560
- :port => @port,
561
- :encryption =>
562
- @encryption)
563
- @open_connection.bind(@auth)
564
- yield self
565
- ensure
566
- @open_connection.close if @open_connection
567
- @open_connection = nil
711
+ instrument "open.net_ldap" do |payload|
712
+ begin
713
+ @open_connection = new_connection
714
+ payload[:connection] = @open_connection
715
+ payload[:bind] = @result = @open_connection.bind(@auth)
716
+ yield self
717
+ ensure
718
+ @open_connection.close if @open_connection
719
+ @open_connection = nil
720
+ end
568
721
  end
569
722
  end
570
723
 
@@ -582,6 +735,9 @@ class Net::LDAP
582
735
  # Net::LDAP::SearchScope_WholeSubtree. Default is WholeSubtree.)
583
736
  # * :size (an integer indicating the maximum number of search entries to
584
737
  # return. Default is zero, which signifies no limit.)
738
+ # * :time (an integer restricting the maximum time in seconds allowed for a search. Default is zero, no time limit RFC 4511 4.5.1.5)
739
+ # * :deref (one of: Net::LDAP::DerefAliases_Never, Net::LDAP::DerefAliases_Search,
740
+ # Net::LDAP::DerefAliases_Find, Net::LDAP::DerefAliases_Always. Default is Never.)
585
741
  #
586
742
  # #search queries the LDAP server and passes <i>each entry</i> to the
587
743
  # caller-supplied block, as an object of type Net::LDAP::Entry. If the
@@ -623,31 +779,23 @@ class Net::LDAP
623
779
  return_result_set = args[:return_result] != false
624
780
  result_set = return_result_set ? [] : nil
625
781
 
626
- if @open_connection
627
- @result = @open_connection.search(args) { |entry|
628
- result_set << entry if result_set
629
- yield entry if block_given?
630
- }
631
- else
632
- @result = 0
633
- begin
634
- conn = Net::LDAP::Connection.new(:host => @host, :port => @port,
635
- :encryption => @encryption)
636
- if (@result = conn.bind(args[:auth] || @auth)) == 0
637
- @result = conn.search(args) { |entry|
638
- result_set << entry if result_set
639
- yield entry if block_given?
640
- }
782
+ instrument "search.net_ldap", args do |payload|
783
+ @result = use_connection(args) do |conn|
784
+ conn.search(args) do |entry|
785
+ result_set << entry if result_set
786
+ yield entry if block_given?
641
787
  end
642
- ensure
643
- conn.close if conn
644
788
  end
645
- end
646
789
 
647
- if return_result_set
648
- @result == 0 ? result_set : nil
649
- else
650
- @result == 0
790
+ if return_result_set
791
+ unless @result.nil?
792
+ if ResultCodesSearchSuccess.include?(@result.result_code)
793
+ result_set
794
+ end
795
+ end
796
+ else
797
+ @result.success?
798
+ end
651
799
  end
652
800
  end
653
801
 
@@ -709,19 +857,22 @@ class Net::LDAP
709
857
  # the documentation for #auth, the password parameter can be a Ruby Proc
710
858
  # instead of a String.
711
859
  def bind(auth = @auth)
712
- if @open_connection
713
- @result = @open_connection.bind(auth)
714
- else
715
- begin
716
- conn = Connection.new(:host => @host, :port => @port,
717
- :encryption => @encryption)
718
- @result = conn.bind(auth)
719
- ensure
720
- conn.close if conn
860
+ instrument "bind.net_ldap" do |payload|
861
+ if @open_connection
862
+ payload[:connection] = @open_connection
863
+ payload[:bind] = @result = @open_connection.bind(auth)
864
+ else
865
+ begin
866
+ conn = new_connection
867
+ payload[:connection] = conn
868
+ payload[:bind] = @result = conn.bind(auth)
869
+ ensure
870
+ conn.close if conn
871
+ end
721
872
  end
722
- end
723
873
 
724
- @result == 0
874
+ @result.success?
875
+ end
725
876
  end
726
877
 
727
878
  # #bind_as is for testing authentication credentials.
@@ -772,7 +923,7 @@ class Net::LDAP
772
923
  # end
773
924
  def bind_as(args = {})
774
925
  result = false
775
- open { |me|
926
+ open do |me|
776
927
  rs = search args
777
928
  if rs and rs.first and dn = rs.first.dn
778
929
  password = args[:password]
@@ -780,7 +931,7 @@ class Net::LDAP
780
931
  result = rs if bind(:method => :simple, :username => dn,
781
932
  :password => password)
782
933
  end
783
- }
934
+ end
784
935
  result
785
936
  end
786
937
 
@@ -809,21 +960,12 @@ class Net::LDAP
809
960
  # ldap.add(:dn => dn, :attributes => attr)
810
961
  # end
811
962
  def add(args)
812
- if @open_connection
813
- @result = @open_connection.add(args)
814
- else
815
- @result = 0
816
- begin
817
- conn = Connection.new(:host => @host, :port => @port,
818
- :encryption => @encryption)
819
- if (@result = conn.bind(args[:auth] || @auth)) == 0
820
- @result = conn.add(args)
821
- end
822
- ensure
823
- conn.close if conn
963
+ instrument "add.net_ldap", args do |payload|
964
+ @result = use_connection(args) do |conn|
965
+ conn.add(args)
824
966
  end
967
+ @result.success?
825
968
  end
826
- @result == 0
827
969
  end
828
970
 
829
971
  # Modifies the attribute values of a particular entry on the LDAP
@@ -841,7 +983,7 @@ class Net::LDAP
841
983
  # The LDAP protocol provides a full and well thought-out set of operations
842
984
  # for changing the values of attributes, but they are necessarily somewhat
843
985
  # complex and not always intuitive. If these instructions are confusing or
844
- # incomplete, please send us email or create a bug report on rubyforge.
986
+ # incomplete, please send us email or create an issue on GitHub.
845
987
  #
846
988
  # The :operations parameter to #modify takes an array of
847
989
  # operation-descriptors. Each individual operation is specified in one
@@ -849,9 +991,10 @@ class Net::LDAP
849
991
  # operations in order.
850
992
  #
851
993
  # Each of the operations appearing in the Array must itself be an Array
852
- # with exactly three elements: an operator:: must be :add, :replace, or
853
- # :delete an attribute name:: the attribute name (string or symbol) to
854
- # modify a value:: either a string or an array of strings.
994
+ # with exactly three elements:
995
+ # an operator :: must be :add, :replace, or :delete
996
+ # an attribute name :: the attribute name (string or symbol) to modify
997
+ # a value :: either a string or an array of strings.
855
998
  #
856
999
  # The :add operator will, unsurprisingly, add the specified values to the
857
1000
  # specified attribute. If the attribute does not already exist, :add will
@@ -894,34 +1037,63 @@ class Net::LDAP
894
1037
  # may not get extended information that will tell you which one failed.
895
1038
  # #modify has no notion of an atomic transaction. If you specify a chain
896
1039
  # of modifications in one call to #modify, and one of them fails, the
897
- # preceding ones will usually not be "rolled back, " resulting in a
1040
+ # preceding ones will usually not be "rolled back", resulting in a
898
1041
  # partial update. This is a limitation of the LDAP protocol, not of
899
1042
  # Net::LDAP.
900
1043
  #
901
1044
  # The lack of transactional atomicity in LDAP means that you're usually
902
1045
  # better off using the convenience methods #add_attribute,
903
- # #replace_attribute, and #delete_attribute, which are are wrappers over
1046
+ # #replace_attribute, and #delete_attribute, which are wrappers over
904
1047
  # #modify. However, certain LDAP servers may provide concurrency
905
1048
  # semantics, in which the several operations contained in a single #modify
906
1049
  # call are not interleaved with other modification-requests received
907
1050
  # simultaneously by the server. It bears repeating that this concurrency
908
1051
  # does _not_ imply transactional atomicity, which LDAP does not provide.
909
1052
  def modify(args)
910
- if @open_connection
911
- @result = @open_connection.modify(args)
912
- else
913
- @result = 0
914
- begin
915
- conn = Connection.new(:host => @host, :port => @port,
916
- :encryption => @encryption)
917
- if (@result = conn.bind(args[:auth] || @auth)) == 0
918
- @result = conn.modify(args)
919
- end
920
- ensure
921
- conn.close if conn
1053
+ instrument "modify.net_ldap", args do |payload|
1054
+ @result = use_connection(args) do |conn|
1055
+ conn.modify(args)
1056
+ end
1057
+ @result.success?
1058
+ end
1059
+ end
1060
+
1061
+ # Password Modify
1062
+ #
1063
+ # Change existing password:
1064
+ #
1065
+ # dn = 'uid=modify-password-user1,ou=People,dc=rubyldap,dc=com'
1066
+ # auth = {
1067
+ # method: :simple,
1068
+ # username: dn,
1069
+ # password: 'passworD1'
1070
+ # }
1071
+ # ldap.password_modify(dn: dn,
1072
+ # auth: auth,
1073
+ # old_password: 'passworD1',
1074
+ # new_password: 'passworD2')
1075
+ #
1076
+ # Or get the LDAP server to generate a password for you:
1077
+ #
1078
+ # dn = 'uid=modify-password-user1,ou=People,dc=rubyldap,dc=com'
1079
+ # auth = {
1080
+ # method: :simple,
1081
+ # username: dn,
1082
+ # password: 'passworD1'
1083
+ # }
1084
+ # ldap.password_modify(dn: dn,
1085
+ # auth: auth,
1086
+ # old_password: 'passworD1')
1087
+ #
1088
+ # ldap.get_operation_result.extended_response[0][0] #=> 'VtcgGf/G'
1089
+ #
1090
+ def password_modify(args)
1091
+ instrument "modify_password.net_ldap", args do |payload|
1092
+ @result = use_connection(args) do |conn|
1093
+ conn.password_modify(args)
922
1094
  end
1095
+ @result.success?
923
1096
  end
924
- @result == 0
925
1097
  end
926
1098
 
927
1099
  # Add a value to an attribute. Takes the full DN of the entry to modify,
@@ -978,21 +1150,12 @@ class Net::LDAP
978
1150
  #
979
1151
  # _Documentation_ _stub_
980
1152
  def rename(args)
981
- if @open_connection
982
- @result = @open_connection.rename(args)
983
- else
984
- @result = 0
985
- begin
986
- conn = Connection.new(:host => @host, :port => @port,
987
- :encryption => @encryption)
988
- if (@result = conn.bind(args[:auth] || @auth)) == 0
989
- @result = conn.rename(args)
990
- end
991
- ensure
992
- conn.close if conn
1153
+ instrument "rename.net_ldap", args do |payload|
1154
+ @result = use_connection(args) do |conn|
1155
+ conn.rename(args)
993
1156
  end
1157
+ @result.success?
994
1158
  end
995
- @result == 0
996
1159
  end
997
1160
  alias_method :modify_rdn, :rename
998
1161
 
@@ -1006,21 +1169,33 @@ class Net::LDAP
1006
1169
  # dn = "mail=deleteme@example.com, ou=people, dc=example, dc=com"
1007
1170
  # ldap.delete :dn => dn
1008
1171
  def delete(args)
1009
- if @open_connection
1010
- @result = @open_connection.delete(args)
1011
- else
1012
- @result = 0
1013
- begin
1014
- conn = Connection.new(:host => @host, :port => @port,
1015
- :encryption => @encryption)
1016
- if (@result = conn.bind(args[:auth] || @auth)) == 0
1017
- @result = conn.delete(args)
1018
- end
1019
- ensure
1020
- conn.close
1172
+ instrument "delete.net_ldap", args do |payload|
1173
+ @result = use_connection(args) do |conn|
1174
+ conn.delete(args)
1021
1175
  end
1176
+ @result.success?
1177
+ end
1178
+ end
1179
+
1180
+ # Delete an entry from the LDAP directory along with all subordinate entries.
1181
+ # the regular delete method will fail to delete an entry if it has subordinate
1182
+ # entries. This method sends an extra control code to tell the LDAP server
1183
+ # to do a tree delete. ('1.2.840.113556.1.4.805')
1184
+ #
1185
+ # If the LDAP server does not support the DELETE_TREE control code, subordinate
1186
+ # entries are deleted recursively instead.
1187
+ #
1188
+ # Returns True or False to indicate whether the delete succeeded. Extended
1189
+ # status information is available by calling #get_operation_result.
1190
+ #
1191
+ # dn = "mail=deleteme@example.com, ou=people, dc=example, dc=com"
1192
+ # ldap.delete_tree :dn => dn
1193
+ def delete_tree(args)
1194
+ if search_root_dse[:supportedcontrol].include? Net::LDAP::LDAPControls::DELETE_TREE
1195
+ delete(args.merge(:control_codes => [[Net::LDAP::LDAPControls::DELETE_TREE, true]]))
1196
+ else
1197
+ recursive_delete(args)
1022
1198
  end
1023
- @result == 0
1024
1199
  end
1025
1200
 
1026
1201
  # This method is experimental and subject to change. Return the rootDSE
@@ -1039,9 +1214,16 @@ class Net::LDAP
1039
1214
  def search_root_dse
1040
1215
  rs = search(:ignore_server_caps => true, :base => "",
1041
1216
  :scope => SearchScope_BaseObject,
1042
- :attributes => [ :namingContexts, :supportedLdapVersion,
1043
- :altServer, :supportedControl, :supportedExtension,
1044
- :supportedFeatures, :supportedSASLMechanisms])
1217
+ :attributes => [
1218
+ :altServer,
1219
+ :namingContexts,
1220
+ :supportedCapabilities,
1221
+ :supportedControl,
1222
+ :supportedExtension,
1223
+ :supportedFeatures,
1224
+ :supportedLdapVersion,
1225
+ :supportedSASLMechanisms,
1226
+ ])
1045
1227
  (rs and rs.first) or Net::LDAP::Entry.new
1046
1228
  end
1047
1229
 
@@ -1092,473 +1274,93 @@ class Net::LDAP
1092
1274
  # MUST refactor the root_dse call out.
1093
1275
  #++
1094
1276
  def paged_searches_supported?
1277
+ # active directory returns that it supports paged results. However
1278
+ # it returns binary data in the rfc2696_cookie which throws an
1279
+ # encoding exception breaking searching.
1280
+ return false if @force_no_page
1095
1281
  @server_caps ||= search_root_dse
1096
- @server_caps[:supportedcontrol].include?(Net::LDAP::LdapControls::PagedResults)
1282
+ @server_caps[:supportedcontrol].include?(Net::LDAP::LDAPControls::PAGED_RESULTS)
1097
1283
  end
1098
- end # class LDAP
1099
-
1100
- # This is a private class used internally by the library. It should not
1101
- # be called by user code.
1102
- class Net::LDAP::Connection #:nodoc:
1103
- LdapVersion = 3
1104
- MaxSaslChallenges = 10
1105
1284
 
1106
- def initialize(server)
1107
- begin
1108
- @conn = TCPSocket.new(server[:host], server[:port])
1109
- rescue SocketError
1110
- raise Net::LDAP::LdapError, "No such address or other socket error."
1111
- rescue Errno::ECONNREFUSED
1112
- raise Net::LDAP::LdapError, "Server #{server[:host]} refused connection on port #{server[:port]}."
1113
- end
1114
-
1115
- if server[:encryption]
1116
- setup_encryption server[:encryption]
1117
- end
1118
-
1119
- yield self if block_given?
1285
+ # Mask auth password
1286
+ def inspect
1287
+ inspected = super
1288
+ inspected.gsub! @auth[:password], "*******" if @auth[:password]
1289
+ inspected
1120
1290
  end
1121
1291
 
1122
- module GetbyteForSSLSocket
1123
- def getbyte
1124
- getc.ord
1125
- end
1292
+ # Internal: Set @open_connection for testing
1293
+ def connection=(connection)
1294
+ @open_connection = connection
1126
1295
  end
1127
1296
 
1128
- def self.wrap_with_ssl(io)
1129
- raise Net::LDAP::LdapError, "OpenSSL is unavailable" unless Net::LDAP::HasOpenSSL
1130
- ctx = OpenSSL::SSL::SSLContext.new
1131
- conn = OpenSSL::SSL::SSLSocket.new(io, ctx)
1132
- conn.connect
1133
- conn.sync_close = true
1297
+ private
1134
1298
 
1135
- conn.extend(GetbyteForSSLSocket) unless conn.respond_to?(:getbyte)
1136
-
1137
- conn
1138
- end
1139
-
1140
- #--
1141
- # Helper method called only from new, and only after we have a
1142
- # successfully-opened @conn instance variable, which is a TCP connection.
1143
- # Depending on the received arguments, we establish SSL, potentially
1144
- # replacing the value of @conn accordingly. Don't generate any errors here
1145
- # if no encryption is requested. DO raise Net::LDAP::LdapError objects if encryption
1146
- # is requested and we have trouble setting it up. That includes if OpenSSL
1147
- # is not set up on the machine. (Question: how does the Ruby OpenSSL
1148
- # wrapper react in that case?) DO NOT filter exceptions raised by the
1149
- # OpenSSL library. Let them pass back to the user. That should make it
1150
- # easier for us to debug the problem reports. Presumably (hopefully?) that
1151
- # will also produce recognizable errors if someone tries to use this on a
1152
- # machine without OpenSSL.
1153
- #
1154
- # The simple_tls method is intended as the simplest, stupidest, easiest
1155
- # solution for people who want nothing more than encrypted comms with the
1156
- # LDAP server. It doesn't do any server-cert validation and requires
1157
- # nothing in the way of key files and root-cert files, etc etc. OBSERVE:
1158
- # WE REPLACE the value of @conn, which is presumed to be a connected
1159
- # TCPSocket object.
1160
- #
1161
- # The start_tls method is supported by many servers over the standard LDAP
1162
- # port. It does not require an alternative port for encrypted
1163
- # communications, as with simple_tls. Thanks for Kouhei Sutou for
1164
- # generously contributing the :start_tls path.
1165
- #++
1166
- def setup_encryption(args)
1167
- case args[:method]
1168
- when :simple_tls
1169
- @conn = self.class.wrap_with_ssl(@conn)
1170
- # additional branches requiring server validation and peer certs, etc.
1171
- # go here.
1172
- when :start_tls
1173
- msgid = next_msgid.to_ber
1174
- request = [Net::LDAP::StartTlsOid.to_ber].to_ber_appsequence(Net::LDAP::PDU::ExtendedRequest)
1175
- request_pkt = [msgid, request].to_ber_sequence
1176
- @conn.write request_pkt
1177
- be = @conn.read_ber(Net::LDAP::AsnSyntax)
1178
- raise Net::LDAP::LdapError, "no start_tls result" if be.nil?
1179
- pdu = Net::LDAP::PDU.new(be)
1180
- raise Net::LDAP::LdapError, "no start_tls result" if pdu.nil?
1181
- if pdu.result_code.zero?
1182
- @conn = self.class.wrap_with_ssl(@conn)
1183
- else
1184
- raise Net::LDAP::LdapError, "start_tls failed: #{pdu.result_code}"
1185
- end
1186
- else
1187
- raise Net::LDAP::LdapError, "unsupported encryption method #{args[:method]}"
1188
- end
1189
- end
1190
-
1191
- #--
1192
- # This is provided as a convenience method to make sure a connection
1193
- # object gets closed without waiting for a GC to happen. Clients shouldn't
1194
- # have to call it, but perhaps it will come in handy someday.
1195
- #++
1196
- def close
1197
- @conn.close
1198
- @conn = nil
1199
- end
1200
-
1201
- def next_msgid
1202
- @msgid ||= 0
1203
- @msgid += 1
1204
- end
1205
-
1206
- def bind(auth)
1207
- meth = auth[:method]
1208
- if [:simple, :anonymous, :anon].include?(meth)
1209
- bind_simple auth
1210
- elsif meth == :sasl
1211
- bind_sasl(auth)
1212
- elsif meth == :gss_spnego
1213
- bind_gss_spnego(auth)
1299
+ # Yields an open connection if there is one, otherwise establishes a new
1300
+ # connection, binds, and yields it. If binding fails, it will return the
1301
+ # result from that, and :use_connection: will not yield at all. If not
1302
+ # the return value is whatever is returned from the block.
1303
+ def use_connection(args)
1304
+ if @open_connection
1305
+ yield @open_connection
1214
1306
  else
1215
- raise Net::LDAP::LdapError, "Unsupported auth method (#{meth})"
1307
+ begin
1308
+ conn = new_connection
1309
+ result = conn.bind(args[:auth] || @auth)
1310
+ return result unless result.result_code == Net::LDAP::ResultCodeSuccess
1311
+ yield conn
1312
+ ensure
1313
+ conn.close if conn
1314
+ end
1216
1315
  end
1217
1316
  end
1218
1317
 
1219
- #--
1220
- # Implements a simple user/psw authentication. Accessed by calling #bind
1221
- # with a method of :simple or :anonymous.
1222
- #++
1223
- def bind_simple(auth)
1224
- user, psw = if auth[:method] == :simple
1225
- [auth[:username] || auth[:dn], auth[:password]]
1226
- else
1227
- ["", ""]
1228
- end
1229
-
1230
- raise Net::LDAP::LdapError, "Invalid binding information" unless (user && psw)
1231
-
1232
- msgid = next_msgid.to_ber
1233
- request = [LdapVersion.to_ber, user.to_ber,
1234
- psw.to_ber_contextspecific(0)].to_ber_appsequence(0)
1235
- request_pkt = [msgid, request].to_ber_sequence
1236
- @conn.write request_pkt
1237
-
1238
- (be = @conn.read_ber(Net::LDAP::AsnSyntax) and pdu = Net::LDAP::PDU.new(be)) or raise Net::LDAP::LdapError, "no bind result"
1239
-
1240
- pdu.result_code
1241
- end
1242
-
1243
- #--
1244
- # Required parameters: :mechanism, :initial_credential and
1245
- # :challenge_response
1246
- #
1247
- # Mechanism is a string value that will be passed in the SASL-packet's
1248
- # "mechanism" field.
1249
- #
1250
- # Initial credential is most likely a string. It's passed in the initial
1251
- # BindRequest that goes to the server. In some protocols, it may be empty.
1252
- #
1253
- # Challenge-response is a Ruby proc that takes a single parameter and
1254
- # returns an object that will typically be a string. The
1255
- # challenge-response block is called when the server returns a
1256
- # BindResponse with a result code of 14 (saslBindInProgress). The
1257
- # challenge-response block receives a parameter containing the data
1258
- # returned by the server in the saslServerCreds field of the LDAP
1259
- # BindResponse packet. The challenge-response block may be called multiple
1260
- # times during the course of a SASL authentication, and each time it must
1261
- # return a value that will be passed back to the server as the credential
1262
- # data in the next BindRequest packet.
1263
- #++
1264
- def bind_sasl(auth)
1265
- mech, cred, chall = auth[:mechanism], auth[:initial_credential],
1266
- auth[:challenge_response]
1267
- raise Net::LDAP::LdapError, "Invalid binding information" unless (mech && cred && chall)
1268
-
1269
- n = 0
1270
- loop {
1271
- msgid = next_msgid.to_ber
1272
- sasl = [mech.to_ber, cred.to_ber].to_ber_contextspecific(3)
1273
- request = [LdapVersion.to_ber, "".to_ber, sasl].to_ber_appsequence(0)
1274
- request_pkt = [msgid, request].to_ber_sequence
1275
- @conn.write request_pkt
1276
-
1277
- (be = @conn.read_ber(Net::LDAP::AsnSyntax) and pdu = Net::LDAP::PDU.new(be)) or raise Net::LDAP::LdapError, "no bind result"
1278
- return pdu.result_code unless pdu.result_code == 14 # saslBindInProgress
1279
- raise Net::LDAP::LdapError, "sasl-challenge overflow" if ((n += 1) > MaxSaslChallenges)
1280
-
1281
- cred = chall.call(pdu.result_server_sasl_creds)
1318
+ # Establish a new connection to the LDAP server
1319
+ def new_connection
1320
+ connection = Net::LDAP::Connection.new \
1321
+ :host => @host,
1322
+ :port => @port,
1323
+ :hosts => @hosts,
1324
+ :encryption => @encryption,
1325
+ :instrumentation_service => @instrumentation_service,
1326
+ :connect_timeout => @connect_timeout
1327
+
1328
+ # Force connect to see if there's a connection error
1329
+ connection.socket
1330
+ connection
1331
+ rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
1332
+ @result = {
1333
+ :resultCode => 52,
1334
+ :errorMessage => ResultStrings[ResultCodeUnavailable],
1282
1335
  }
1283
-
1284
- raise Net::LDAP::LdapError, "why are we here?"
1285
- end
1286
- private :bind_sasl
1287
-
1288
- #--
1289
- # PROVISIONAL, only for testing SASL implementations. DON'T USE THIS YET.
1290
- # Uses Kohei Kajimoto's Ruby/NTLM. We have to find a clean way to
1291
- # integrate it without introducing an external dependency.
1292
- #
1293
- # This authentication method is accessed by calling #bind with a :method
1294
- # parameter of :gss_spnego. It requires :username and :password
1295
- # attributes, just like the :simple authentication method. It performs a
1296
- # GSS-SPNEGO authentication with the server, which is presumed to be a
1297
- # Microsoft Active Directory.
1298
- #++
1299
- def bind_gss_spnego(auth)
1300
- require 'ntlm'
1301
-
1302
- user, psw = [auth[:username] || auth[:dn], auth[:password]]
1303
- raise Net::LDAP::LdapError, "Invalid binding information" unless (user && psw)
1304
-
1305
- nego = proc { |challenge|
1306
- t2_msg = NTLM::Message.parse(challenge)
1307
- t3_msg = t2_msg.response({ :user => user, :password => psw },
1308
- { :ntlmv2 => true })
1309
- t3_msg.serialize
1310
- }
1311
-
1312
- bind_sasl(:method => :sasl, :mechanism => "GSS-SPNEGO",
1313
- :initial_credential => NTLM::Message::Type1.new.serialize,
1314
- :challenge_response => nego)
1315
- end
1316
- private :bind_gss_spnego
1317
-
1318
- #--
1319
- # Alternate implementation, this yields each search entry to the caller as
1320
- # it are received.
1321
- #
1322
- # TODO: certain search parameters are hardcoded.
1323
- # TODO: if we mis-parse the server results or the results are wrong, we
1324
- # can block forever. That's because we keep reading results until we get a
1325
- # type-5 packet, which might never come. We need to support the time-limit
1326
- # in the protocol.
1327
- #++
1328
- def search(args = {})
1329
- search_filter = (args && args[:filter]) ||
1330
- Net::LDAP::Filter.eq("objectclass", "*")
1331
- search_filter = Net::LDAP::Filter.construct(search_filter) if search_filter.is_a?(String)
1332
- search_base = (args && args[:base]) || "dc=example, dc=com"
1333
- search_attributes = ((args && args[:attributes]) || []).map { |attr| attr.to_s.to_ber}
1334
- return_referrals = args && args[:return_referrals] == true
1335
- sizelimit = (args && args[:size].to_i) || 0
1336
- raise Net::LDAP::LdapError, "invalid search-size" unless sizelimit >= 0
1337
- paged_searches_supported = (args && args[:paged_searches_supported])
1338
-
1339
- attributes_only = (args and args[:attributes_only] == true)
1340
- scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree
1341
- raise Net::LDAP::LdapError, "invalid search scope" unless Net::LDAP::SearchScopes.include?(scope)
1342
-
1343
- # An interesting value for the size limit would be close to A/D's
1344
- # built-in page limit of 1000 records, but openLDAP newer than version
1345
- # 2.2.0 chokes on anything bigger than 126. You get a silent error that
1346
- # is easily visible by running slapd in debug mode. Go figure.
1347
- #
1348
- # Changed this around 06Sep06 to support a caller-specified search-size
1349
- # limit. Because we ALWAYS do paged searches, we have to work around the
1350
- # problem that it's not legal to specify a "normal" sizelimit (in the
1351
- # body of the search request) that is larger than the page size we're
1352
- # requesting. Unfortunately, I have the feeling that this will break
1353
- # with LDAP servers that don't support paged searches!!!
1354
- #
1355
- # (Because we pass zero as the sizelimit on search rounds when the
1356
- # remaining limit is larger than our max page size of 126. In these
1357
- # cases, I think the caller's search limit will be ignored!)
1358
- #
1359
- # CONFIRMED: This code doesn't work on LDAPs that don't support paged
1360
- # searches when the size limit is larger than 126. We're going to have
1361
- # to do a root-DSE record search and not do a paged search if the LDAP
1362
- # doesn't support it. Yuck.
1363
- rfc2696_cookie = [126, ""]
1364
- result_code = 0
1365
- n_results = 0
1366
-
1367
- loop {
1368
- # should collect this into a private helper to clarify the structure
1369
- query_limit = 0
1370
- if sizelimit > 0
1371
- if paged_searches_supported
1372
- query_limit = (((sizelimit - n_results) < 126) ? (sizelimit -
1373
- n_results) : 0)
1374
- else
1375
- query_limit = sizelimit
1376
- end
1377
- end
1378
-
1379
- request = [
1380
- search_base.to_ber,
1381
- scope.to_ber_enumerated,
1382
- 0.to_ber_enumerated,
1383
- query_limit.to_ber, # size limit
1384
- 0.to_ber,
1385
- attributes_only.to_ber,
1386
- search_filter.to_ber,
1387
- search_attributes.to_ber_sequence
1388
- ].to_ber_appsequence(3)
1389
-
1390
- controls = []
1391
- controls <<
1392
- [
1393
- Net::LDAP::LdapControls::PagedResults.to_ber,
1394
- # Criticality MUST be false to interoperate with normal LDAPs.
1395
- false.to_ber,
1396
- rfc2696_cookie.map{ |v| v.to_ber}.to_ber_sequence.to_s.to_ber
1397
- ].to_ber_sequence if paged_searches_supported
1398
- controls = controls.empty? ? nil : controls.to_ber_contextspecific(0)
1399
-
1400
- pkt = [next_msgid.to_ber, request, controls].compact.to_ber_sequence
1401
- @conn.write pkt
1402
-
1403
- result_code = 0
1404
- controls = []
1405
-
1406
- while (be = @conn.read_ber(Net::LDAP::AsnSyntax)) && (pdu = Net::LDAP::PDU.new(be))
1407
- case pdu.app_tag
1408
- when 4 # search-data
1409
- n_results += 1
1410
- yield pdu.search_entry if block_given?
1411
- when 19 # search-referral
1412
- if return_referrals
1413
- if block_given?
1414
- se = Net::LDAP::Entry.new
1415
- se[:search_referrals] = (pdu.search_referrals || [])
1416
- yield se
1417
- end
1418
- end
1419
- when 5 # search-result
1420
- result_code = pdu.result_code
1421
- controls = pdu.result_controls
1422
- if return_referrals && result_code == 10
1423
- if block_given?
1424
- se = Net::LDAP::Entry.new
1425
- se[:search_referrals] = (pdu.search_referrals || [])
1426
- yield se
1427
- end
1428
- end
1429
- break
1430
- else
1431
- raise Net::LDAP::LdapError, "invalid response-type in search: #{pdu.app_tag}"
1432
- end
1433
- end
1434
-
1435
- # When we get here, we have seen a type-5 response. If there is no
1436
- # error AND there is an RFC-2696 cookie, then query again for the next
1437
- # page of results. If not, we're done. Don't screw this up or we'll
1438
- # break every search we do.
1439
- #
1440
- # Noticed 02Sep06, look at the read_ber call in this loop, shouldn't
1441
- # that have a parameter of AsnSyntax? Does this just accidentally
1442
- # work? According to RFC-2696, the value expected in this position is
1443
- # of type OCTET STRING, covered in the default syntax supported by
1444
- # read_ber, so I guess we're ok.
1445
- more_pages = false
1446
- if result_code == 0 and controls
1447
- controls.each do |c|
1448
- if c.oid == Net::LDAP::LdapControls::PagedResults
1449
- # just in case some bogus server sends us more than 1 of these.
1450
- more_pages = false
1451
- if c.value and c.value.length > 0
1452
- cookie = c.value.read_ber[1]
1453
- if cookie and cookie.length > 0
1454
- rfc2696_cookie[1] = cookie
1455
- more_pages = true
1456
- end
1457
- end
1458
- end
1459
- end
1460
- end
1461
-
1462
- break unless more_pages
1463
- } # loop
1464
-
1465
- result_code
1336
+ raise e
1466
1337
  end
1467
1338
 
1468
- MODIFY_OPERATIONS = { #:nodoc:
1469
- :add => 0,
1470
- :delete => 1,
1471
- :replace => 2
1472
- }
1339
+ # Normalize encryption parameter the constructor accepts, expands a few
1340
+ # convenience symbols into recognizable hashes
1341
+ def normalize_encryption(args)
1342
+ return if args.nil?
1343
+ return args if args.is_a? Hash
1473
1344
 
1474
- def self.modify_ops(operations)
1475
- ops = []
1476
- if operations
1477
- operations.each { |op, attrib, values|
1478
- # TODO, fix the following line, which gives a bogus error if the
1479
- # opcode is invalid.
1480
- op_ber = MODIFY_OPERATIONS[op.to_sym].to_ber_enumerated
1481
- values = [ values ].flatten.map { |v| v.to_ber if v }.to_ber_set
1482
- values = [ attrib.to_s.to_ber, values ].to_ber_sequence
1483
- ops << [ op_ber, values ].to_ber
1484
- }
1345
+ case method = args.to_sym
1346
+ when :simple_tls, :start_tls
1347
+ { :method => method, :tls_options => {} }
1485
1348
  end
1486
- ops
1487
1349
  end
1488
1350
 
1489
- #--
1490
- # TODO: need to support a time limit, in case the server fails to respond.
1491
- # TODO: We're throwing an exception here on empty DN. Should return a
1492
- # proper error instead, probaby from farther up the chain.
1493
- # TODO: If the user specifies a bogus opcode, we'll throw a confusing
1494
- # error here ("to_ber_enumerated is not defined on nil").
1495
- #++
1496
- def modify(args)
1497
- modify_dn = args[:dn] or raise "Unable to modify empty DN"
1498
- ops = self.class.modify_ops args[:operations]
1499
- request = [ modify_dn.to_ber,
1500
- ops.to_ber_sequence ].to_ber_appsequence(6)
1501
- pkt = [ next_msgid.to_ber, request ].to_ber_sequence
1502
- @conn.write pkt
1503
-
1504
- (be = @conn.read_ber(Net::LDAP::AsnSyntax)) && (pdu = Net::LDAP::PDU.new(be)) && (pdu.app_tag == 7) or raise Net::LDAP::LdapError, "response missing or invalid"
1505
- pdu.result_code
1506
- end
1507
-
1508
- #--
1509
- # TODO: need to support a time limit, in case the server fails to respond.
1510
- # Unlike other operation-methods in this class, we return a result hash
1511
- # rather than a simple result number. This is experimental, and eventually
1512
- # we'll want to do this with all the others. The point is to have access
1513
- # to the error message and the matched-DN returned by the server.
1514
- #++
1515
- def add(args)
1516
- add_dn = args[:dn] or raise Net::LDAP::LdapError, "Unable to add empty DN"
1517
- add_attrs = []
1518
- a = args[:attributes] and a.each { |k, v|
1519
- add_attrs << [ k.to_s.to_ber, Array(v).map { |m| m.to_ber}.to_ber_set ].to_ber_sequence
1520
- }
1521
-
1522
- request = [add_dn.to_ber, add_attrs.to_ber_sequence].to_ber_appsequence(8)
1523
- pkt = [next_msgid.to_ber, request].to_ber_sequence
1524
- @conn.write pkt
1525
-
1526
- (be = @conn.read_ber(Net::LDAP::AsnSyntax)) && (pdu = Net::LDAP::PDU.new(be)) && (pdu.app_tag == 9) or raise Net::LDAP::LdapError, "response missing or invalid"
1527
- pdu.result_code
1528
- end
1529
-
1530
- #--
1531
- # TODO: need to support a time limit, in case the server fails to respond.
1532
- #++
1533
- def rename args
1534
- old_dn = args[:olddn] or raise "Unable to rename empty DN"
1535
- new_rdn = args[:newrdn] or raise "Unable to rename to empty RDN"
1536
- delete_attrs = args[:delete_attributes] ? true : false
1537
- new_superior = args[:new_superior]
1538
-
1539
- request = [old_dn.to_ber, new_rdn.to_ber, delete_attrs.to_ber]
1540
- request << new_superior.to_ber unless new_superior == nil
1541
-
1542
- pkt = [next_msgid.to_ber, request.to_ber_appsequence(12)].to_ber_sequence
1543
- @conn.write pkt
1544
-
1545
- (be = @conn.read_ber(Net::LDAP::AsnSyntax)) &&
1546
- (pdu = Net::LDAP::PDU.new( be )) && (pdu.app_tag == 13) or
1547
- raise Net::LDAP::LdapError.new( "response missing or invalid" )
1548
- pdu.result_code
1351
+ # Recursively delete a dn and it's subordinate children.
1352
+ # This is useful when a server does not support the DELETE_TREE control code.
1353
+ def recursive_delete(args)
1354
+ raise EmptyDNError unless args.is_a?(Hash) && args.key?(:dn)
1355
+ # Delete Children
1356
+ search(base: args[:dn], scope: Net::LDAP::SearchScope_SingleLevel) do |entry|
1357
+ recursive_delete(dn: entry.dn)
1358
+ end
1359
+ # Delete Self
1360
+ unless delete(dn: args[:dn])
1361
+ raise Net::LDAP::Error, get_operation_result[:error_message].to_s
1362
+ end
1363
+ true
1549
1364
  end
1550
1365
 
1551
- #--
1552
- # TODO, need to support a time limit, in case the server fails to respond.
1553
- #++
1554
- def delete(args)
1555
- dn = args[:dn] or raise "Unable to delete empty DN"
1556
-
1557
- request = dn.to_s.to_ber_application_string(10)
1558
- pkt = [next_msgid.to_ber, request].to_ber_sequence
1559
- @conn.write pkt
1560
-
1561
- (be = @conn.read_ber(Net::LDAP::AsnSyntax)) && (pdu = Net::LDAP::PDU.new(be)) && (pdu.app_tag == 11) or raise Net::LDAP::LdapError, "response missing or invalid"
1562
- pdu.result_code
1563
- end
1564
- end # class Connection
1366
+ end # class LDAP