net-ldap 0.3.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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