net-ldap 0.12.0 → 0.19.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 (74) hide show
  1. checksums.yaml +5 -5
  2. data/Contributors.rdoc +1 -0
  3. data/History.rdoc +112 -0
  4. data/README.rdoc +19 -9
  5. data/lib/net/ber/ber_parser.rb +4 -4
  6. data/lib/net/ber/core_ext/array.rb +1 -1
  7. data/lib/net/ber/core_ext/integer.rb +1 -1
  8. data/lib/net/ber/core_ext/string.rb +1 -1
  9. data/lib/net/ber/core_ext.rb +6 -6
  10. data/lib/net/ber.rb +39 -9
  11. data/lib/net/ldap/auth_adapter/gss_spnego.rb +9 -8
  12. data/lib/net/ldap/auth_adapter/sasl.rb +6 -4
  13. data/lib/net/ldap/auth_adapter/simple.rb +1 -1
  14. data/lib/net/ldap/connection.rb +173 -52
  15. data/lib/net/ldap/dataset.rb +3 -5
  16. data/lib/net/ldap/dn.rb +21 -30
  17. data/lib/net/ldap/entry.rb +15 -7
  18. data/lib/net/ldap/error.rb +2 -25
  19. data/lib/net/ldap/filter.rb +15 -8
  20. data/lib/net/ldap/instrumentation.rb +2 -2
  21. data/lib/net/ldap/password.rb +7 -5
  22. data/lib/net/ldap/pdu.rb +27 -3
  23. data/lib/net/ldap/version.rb +1 -1
  24. data/lib/net/ldap.rb +212 -91
  25. data/lib/net/snmp.rb +19 -19
  26. data/lib/net-ldap.rb +1 -1
  27. metadata +27 -96
  28. data/.gitignore +0 -9
  29. data/.rubocop.yml +0 -5
  30. data/.rubocop_todo.yml +0 -462
  31. data/.travis.yml +0 -31
  32. data/CONTRIBUTING.md +0 -54
  33. data/Gemfile +0 -2
  34. data/Rakefile +0 -23
  35. data/net-ldap.gemspec +0 -36
  36. data/script/changelog +0 -47
  37. data/script/install-openldap +0 -112
  38. data/script/package +0 -7
  39. data/script/release +0 -16
  40. data/test/ber/core_ext/test_array.rb +0 -22
  41. data/test/ber/core_ext/test_string.rb +0 -25
  42. data/test/ber/test_ber.rb +0 -145
  43. data/test/fixtures/cacert.pem +0 -20
  44. data/test/fixtures/openldap/memberof.ldif +0 -33
  45. data/test/fixtures/openldap/retcode.ldif +0 -76
  46. data/test/fixtures/openldap/slapd.conf.ldif +0 -67
  47. data/test/fixtures/seed.ldif +0 -374
  48. data/test/integration/test_add.rb +0 -28
  49. data/test/integration/test_ber.rb +0 -30
  50. data/test/integration/test_bind.rb +0 -34
  51. data/test/integration/test_delete.rb +0 -31
  52. data/test/integration/test_open.rb +0 -88
  53. data/test/integration/test_return_codes.rb +0 -38
  54. data/test/integration/test_search.rb +0 -77
  55. data/test/support/vm/openldap/.gitignore +0 -1
  56. data/test/support/vm/openldap/README.md +0 -32
  57. data/test/support/vm/openldap/Vagrantfile +0 -33
  58. data/test/test_auth_adapter.rb +0 -11
  59. data/test/test_dn.rb +0 -44
  60. data/test/test_entry.rb +0 -65
  61. data/test/test_filter.rb +0 -223
  62. data/test/test_filter_parser.rb +0 -24
  63. data/test/test_helper.rb +0 -66
  64. data/test/test_ldap.rb +0 -67
  65. data/test/test_ldap_connection.rb +0 -460
  66. data/test/test_ldif.rb +0 -104
  67. data/test/test_password.rb +0 -10
  68. data/test/test_rename.rb +0 -77
  69. data/test/test_search.rb +0 -39
  70. data/test/test_snmp.rb +0 -119
  71. data/test/test_ssl_ber.rb +0 -40
  72. data/test/testdata.ldif +0 -101
  73. data/testserver/ldapserver.rb +0 -210
  74. data/testserver/testdata.ldif +0 -101
@@ -23,7 +23,7 @@
23
23
  class Net::LDAP::Filter
24
24
  ##
25
25
  # Known filter types.
26
- FilterTypes = [ :ne, :eq, :ge, :le, :and, :or, :not, :ex, :bineq ]
26
+ FilterTypes = [:ne, :eq, :ge, :le, :and, :or, :not, :ex, :bineq]
27
27
 
28
28
  def initialize(op, left, right) #:nodoc:
29
29
  unless FilterTypes.include?(op)
@@ -287,7 +287,7 @@ class Net::LDAP::Filter
287
287
  when 0xa4 # context-specific constructed 4, "substring"
288
288
  str = ""
289
289
  final = false
290
- ber.last.each { |b|
290
+ ber.last.each do |b|
291
291
  case b.ber_identifier
292
292
  when 0x80 # context-specific primitive 0, SubstringFilter "initial"
293
293
  raise Net::LDAP::SubstringFilterError, "Unrecognized substring filter; bad initial value." if str.length > 0
@@ -298,7 +298,7 @@ class Net::LDAP::Filter
298
298
  str += "*#{escape(b)}"
299
299
  final = true
300
300
  end
301
- }
301
+ end
302
302
  str += "*" unless final
303
303
  eq(ber.first.to_s, str)
304
304
  when 0xa5 # context-specific constructed 5, "greaterOrEqual"
@@ -490,7 +490,7 @@ class Net::LDAP::Filter
490
490
  when :eq
491
491
  if @right == "*" # presence test
492
492
  @left.to_s.to_ber_contextspecific(7)
493
- elsif @right =~ /[*]/ # substring
493
+ elsif @right.to_s =~ /[*]/ # substring
494
494
  # Parsing substrings is a little tricky. We use String#split to
495
495
  # break a string into substrings delimited by the * (star)
496
496
  # character. But we also need to know whether there is a star at the
@@ -550,10 +550,10 @@ class Net::LDAP::Filter
550
550
  [self.class.eq(@left, @right).to_ber].to_ber_contextspecific(2)
551
551
  when :and
552
552
  ary = [@left.coalesce(:and), @right.coalesce(:and)].flatten
553
- ary.map {|a| a.to_ber}.to_ber_contextspecific(0)
553
+ ary.map(&:to_ber).to_ber_contextspecific(0)
554
554
  when :or
555
555
  ary = [@left.coalesce(:or), @right.coalesce(:or)].flatten
556
- ary.map {|a| a.to_ber}.to_ber_contextspecific(1)
556
+ ary.map(&:to_ber).to_ber_contextspecific(1)
557
557
  when :not
558
558
  [@left.to_ber].to_ber_contextspecific(2)
559
559
  end
@@ -645,8 +645,15 @@ class Net::LDAP::Filter
645
645
 
646
646
  ##
647
647
  # Converts escaped characters (e.g., "\\28") to unescaped characters
648
+ # @note slawson20170317: Don't attempt to unescape 16 byte binary data which we assume are objectGUIDs
649
+ # The binary form of 5936AE79-664F-44EA-BCCB-5C39399514C6 triggers a BINARY -> UTF-8 conversion error
648
650
  def unescape(right)
649
- right.to_s.gsub(/\\([a-fA-F\d]{2})/) { [$1.hex].pack("U") }
651
+ right = right.to_s
652
+ if right.length == 16 && right.encoding == Encoding::BINARY
653
+ right
654
+ else
655
+ right.to_s.gsub(/\\([a-fA-F\d]{2})/) { [$1.hex].pack("U") }
656
+ end
650
657
  end
651
658
  private :unescape
652
659
 
@@ -748,7 +755,7 @@ class Net::LDAP::Filter
748
755
  # This parses a given expression inside of parentheses.
749
756
  def parse_filter_branch(scanner)
750
757
  scanner.scan(/\s*/)
751
- if token = scanner.scan(/[-\w:.]*[\w]/)
758
+ if token = scanner.scan(/[-\w:.;]*[\w]/)
752
759
  scanner.scan(/\s*/)
753
760
  if op = scanner.scan(/<=|>=|!=|:=|=/)
754
761
  scanner.scan(/\s*/)
@@ -12,8 +12,8 @@ module Net::LDAP::Instrumentation
12
12
  def instrument(event, payload = {})
13
13
  payload = (payload || {}).dup
14
14
  if instrumentation_service
15
- instrumentation_service.instrument(event, payload) do |payload|
16
- payload[:result] = yield(payload) if block_given?
15
+ instrumentation_service.instrument(event, payload) do |instr_payload|
16
+ instr_payload[:result] = yield(instr_payload) if block_given?
17
17
  end
18
18
  else
19
19
  yield(payload) if block_given?
@@ -1,5 +1,6 @@
1
1
  # -*- ruby encoding: utf-8 -*-
2
2
  require 'digest/sha1'
3
+ require 'digest/sha2'
3
4
  require 'digest/md5'
4
5
  require 'base64'
5
6
  require 'securerandom'
@@ -19,20 +20,21 @@ class Net::LDAP::Password
19
20
  # * Should we provide sha1 as a synonym for sha1? I vote no because then
20
21
  # should you also provide ssha1 for symmetry?
21
22
  #
22
- attribute_value = ""
23
23
  def generate(type, str)
24
24
  case type
25
25
  when :md5
26
- attribute_value = '{MD5}' + Base64.encode64(Digest::MD5.digest(str)).chomp!
26
+ '{MD5}' + Base64.strict_encode64(Digest::MD5.digest(str))
27
27
  when :sha
28
- attribute_value = '{SHA}' + Base64.encode64(Digest::SHA1.digest(str)).chomp!
28
+ '{SHA}' + Base64.strict_encode64(Digest::SHA1.digest(str))
29
29
  when :ssha
30
30
  salt = SecureRandom.random_bytes(16)
31
- attribute_value = '{SSHA}' + Base64.encode64(Digest::SHA1.digest(str + salt) + salt).chomp!
31
+ '{SSHA}' + Base64.strict_encode64(Digest::SHA1.digest(str + salt) + salt)
32
+ when :ssha256
33
+ salt = SecureRandom.random_bytes(16)
34
+ '{SSHA256}' + Base64.strict_encode64(Digest::SHA256.digest(str + salt) + salt)
32
35
  else
33
36
  raise Net::LDAP::HashTypeUnsupportedError, "Unsupported password-hash type (#{type})"
34
37
  end
35
- return attribute_value
36
38
  end
37
39
  end
38
40
  end
data/lib/net/ldap/pdu.rb CHANGED
@@ -74,6 +74,7 @@ class Net::LDAP::PDU
74
74
  attr_reader :search_referrals
75
75
  attr_reader :search_parameters
76
76
  attr_reader :bind_parameters
77
+ attr_reader :extended_response
77
78
 
78
79
  ##
79
80
  # Returns RFC-2251 Controls if any.
@@ -120,9 +121,9 @@ class Net::LDAP::PDU
120
121
  when UnbindRequest
121
122
  parse_unbind_request(ber_object[1])
122
123
  when ExtendedResponse
123
- parse_ldap_result(ber_object[1])
124
+ parse_extended_response(ber_object[1])
124
125
  else
125
- raise LdapPduError.new("unknown pdu-type: #{@app_tag}")
126
+ raise Error.new("unknown pdu-type: #{@app_tag}")
126
127
  end
127
128
 
128
129
  parse_controls(ber_object[2]) if ber_object[2]
@@ -174,12 +175,35 @@ class Net::LDAP::PDU
174
175
  @ldap_result = {
175
176
  :resultCode => sequence[0],
176
177
  :matchedDN => sequence[1],
177
- :errorMessage => sequence[2]
178
+ :errorMessage => sequence[2],
178
179
  }
179
180
  parse_search_referral(sequence[3]) if @ldap_result[:resultCode] == Net::LDAP::ResultCodeReferral
180
181
  end
181
182
  private :parse_ldap_result
182
183
 
184
+ ##
185
+ # Parse an extended response
186
+ #
187
+ # http://www.ietf.org/rfc/rfc2251.txt
188
+ #
189
+ # Each Extended operation consists of an Extended request and an
190
+ # Extended response.
191
+ #
192
+ # ExtendedRequest ::= [APPLICATION 23] SEQUENCE {
193
+ # requestName [0] LDAPOID,
194
+ # requestValue [1] OCTET STRING OPTIONAL }
195
+
196
+ def parse_extended_response(sequence)
197
+ sequence.length >= 3 or raise Net::LDAP::PDU::Error, "Invalid LDAP result length."
198
+ @ldap_result = {
199
+ :resultCode => sequence[0],
200
+ :matchedDN => sequence[1],
201
+ :errorMessage => sequence[2],
202
+ }
203
+ @extended_response = sequence[3]
204
+ end
205
+ private :parse_extended_response
206
+
183
207
  ##
184
208
  # A Bind Response may have an additional field, ID [7], serverSaslCreds,
185
209
  # per RFC 2251 pgh 4.2.3.
@@ -1,5 +1,5 @@
1
1
  module Net
2
2
  class LDAP
3
- VERSION = "0.12.0"
3
+ VERSION = "0.19.0"
4
4
  end
5
5
  end
data/lib/net/ldap.rb CHANGED
@@ -17,19 +17,19 @@ 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'
26
- require 'net/ldap/instrumentation'
27
- require 'net/ldap/connection'
28
- require 'net/ldap/version'
29
- require 'net/ldap/error'
30
- require 'net/ldap/auth_adapter'
31
- require 'net/ldap/auth_adapter/simple'
32
- require 'net/ldap/auth_adapter/sasl'
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
33
 
34
34
  Net::LDAP::AuthAdapter.register([:simple, :anon, :anonymous], Net::LDAP::AuthAdapter::Simple)
35
35
  Net::LDAP::AuthAdapter.register(:sasl, Net::LDAP::AuthAdapter::Sasl)
@@ -79,6 +79,14 @@ Net::LDAP::AuthAdapter.register(:sasl, Net::LDAP::AuthAdapter::Sasl)
79
79
  #
80
80
  # p ldap.get_operation_result
81
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
82
90
  #
83
91
  # == A Brief Introduction to LDAP
84
92
  #
@@ -256,14 +264,14 @@ class Net::LDAP
256
264
  SearchScope_BaseObject = 0
257
265
  SearchScope_SingleLevel = 1
258
266
  SearchScope_WholeSubtree = 2
259
- SearchScopes = [ SearchScope_BaseObject, SearchScope_SingleLevel,
260
- SearchScope_WholeSubtree ]
267
+ SearchScopes = [SearchScope_BaseObject, SearchScope_SingleLevel,
268
+ SearchScope_WholeSubtree]
261
269
 
262
270
  DerefAliases_Never = 0
263
271
  DerefAliases_Search = 1
264
272
  DerefAliases_Find = 2
265
273
  DerefAliases_Always = 3
266
- DerefAliasesArray = [ DerefAliases_Never, DerefAliases_Search, DerefAliases_Find, DerefAliases_Always ]
274
+ DerefAliasesArray = [DerefAliases_Never, DerefAliases_Search, DerefAliases_Find, DerefAliases_Always]
267
275
 
268
276
  primitive = { 2 => :null } # UnbindRequest body
269
277
  constructed = {
@@ -315,7 +323,14 @@ class Net::LDAP
315
323
  :constructed => constructed,
316
324
  }
317
325
 
326
+ universal = {
327
+ constructed: {
328
+ 107 => :array, #ExtendedResponse (PasswdModifyResponseValue)
329
+ },
330
+ }
331
+
318
332
  AsnSyntax = Net::BER.compile_syntax(:application => application,
333
+ :universal => universal,
319
334
  :context_specific => context_specific)
320
335
 
321
336
  DefaultHost = "127.0.0.1"
@@ -324,7 +339,8 @@ class Net::LDAP
324
339
  DefaultTreebase = "dc=com"
325
340
  DefaultForceNoPage = false
326
341
 
327
- StartTlsOid = "1.3.6.1.4.1.1466.20037"
342
+ StartTlsOid = '1.3.6.1.4.1.1466.20037'
343
+ PasswdModifyOid = '1.3.6.1.4.1.4203.1.11.1'
328
344
 
329
345
  # https://tools.ietf.org/html/rfc4511#section-4.1.9
330
346
  # https://tools.ietf.org/html/rfc4511#appendix-A
@@ -373,14 +389,14 @@ class Net::LDAP
373
389
  ResultCodeCompareFalse,
374
390
  ResultCodeCompareTrue,
375
391
  ResultCodeReferral,
376
- ResultCodeSaslBindInProgress
392
+ ResultCodeSaslBindInProgress,
377
393
  ]
378
394
 
379
395
  # nonstandard list of "successful" result codes for searches
380
396
  ResultCodesSearchSuccess = [
381
397
  ResultCodeSuccess,
382
398
  ResultCodeTimeLimitExceeded,
383
- ResultCodeSizeLimitExceeded
399
+ ResultCodeSizeLimitExceeded,
384
400
  ]
385
401
 
386
402
  # map of result code to human message
@@ -396,7 +412,7 @@ class Net::LDAP
396
412
  ResultCodeStrongerAuthRequired => "Stronger Auth Needed",
397
413
  ResultCodeReferral => "Referral",
398
414
  ResultCodeAdminLimitExceeded => "Admin Limit Exceeded",
399
- ResultCodeUnavailableCriticalExtension => "Unavailable crtical extension",
415
+ ResultCodeUnavailableCriticalExtension => "Unavailable critical extension",
400
416
  ResultCodeConfidentialityRequired => "Confidentiality Required",
401
417
  ResultCodeSaslBindInProgress => "saslBindInProgress",
402
418
  ResultCodeNoSuchAttribute => "No Such Attribute",
@@ -422,7 +438,7 @@ class Net::LDAP
422
438
  ResultCodeEntryAlreadyExists => "Entry Already Exists",
423
439
  ResultCodeObjectClassModsProhibited => "ObjectClass Modifications Prohibited",
424
440
  ResultCodeAffectsMultipleDSAs => "Affects Multiple DSAs",
425
- ResultCodeOther => "Other"
441
+ ResultCodeOther => "Other",
426
442
  }
427
443
 
428
444
  module LDAPControls
@@ -460,20 +476,75 @@ class Net::LDAP
460
476
  # specify a treebase. If you give a treebase value in any particular
461
477
  # call to #search, that value will override any treebase value you give
462
478
  # here.
463
- # * :encryption => specifies the encryption to be used in communicating
464
- # with the LDAP server. The value is either a Hash containing additional
465
- # parameters, or the Symbol :simple_tls, which is equivalent to
466
- # specifying the Hash {:method => :simple_tls}. There is a fairly large
467
- # range of potential values that may be given for this parameter. See
468
- # #encryption for details.
469
479
  # * :force_no_page => Set to true to prevent paged results even if your
470
480
  # server says it supports them. This is a fix for MS Active Directory
471
481
  # * :instrumentation_service => An object responsible for instrumenting
472
482
  # operations, compatible with ActiveSupport::Notifications' public API.
483
+ # * :connect_timeout => The TCP socket timeout (in seconds) to use when
484
+ # connecting to the LDAP server (default 5 seconds).
485
+ # * :encryption => specifies the encryption to be used in communicating
486
+ # with the LDAP server. The value must be a Hash containing additional
487
+ # parameters, which consists of two keys:
488
+ # method: - :simple_tls or :start_tls
489
+ # tls_options: - Hash of options for that method
490
+ # The :simple_tls encryption method encrypts <i>all</i> communications
491
+ # with the LDAP server. It completely establishes SSL/TLS encryption with
492
+ # the LDAP server before any LDAP-protocol data is exchanged. There is no
493
+ # plaintext negotiation and no special encryption-request controls are
494
+ # sent to the server. <i>The :simple_tls option is the simplest, easiest
495
+ # way to encrypt communications between Net::LDAP and LDAP servers.</i>
496
+ # If you get communications or protocol errors when using this option,
497
+ # check with your LDAP server administrator. Pay particular attention
498
+ # to the TCP port you are connecting to. It's impossible for an LDAP
499
+ # server to support plaintext LDAP communications and <i>simple TLS</i>
500
+ # connections on the same port. The standard TCP port for unencrypted
501
+ # LDAP connections is 389, but the standard port for simple-TLS
502
+ # encrypted connections is 636. Be sure you are using the correct port.
503
+ # The :start_tls like the :simple_tls encryption method also encrypts all
504
+ # communcations with the LDAP server. With the exception that it operates
505
+ # over the standard TCP port.
506
+ #
507
+ # To validate the LDAP server's certificate (a security must if you're
508
+ # talking over the public internet), you need to set :tls_options
509
+ # something like this...
510
+ #
511
+ # Net::LDAP.new(
512
+ # # ... set host, bind dn, etc ...
513
+ # encryption: {
514
+ # method: :simple_tls,
515
+ # tls_options: OpenSSL::SSL::SSLContext::DEFAULT_PARAMS,
516
+ # }
517
+ # )
518
+ #
519
+ # The above will use the operating system-provided store of CA
520
+ # certificates to validate your LDAP server's cert.
521
+ # If cert validation fails, it'll happen during the #bind
522
+ # whenever you first try to open a connection to the server.
523
+ # Those methods will throw Net::LDAP::ConnectionError with
524
+ # a message about certificate verify failing. If your
525
+ # LDAP server's certificate is signed by DigiCert, Comodo, etc.,
526
+ # you're probably good. If you've got a self-signed cert but it's
527
+ # been added to the host's OS-maintained CA store (e.g. on Debian
528
+ # add foobar.crt to /usr/local/share/ca-certificates/ and run
529
+ # `update-ca-certificates`), then the cert should pass validation.
530
+ # To ignore the OS's CA store, put your CA in a PEM-encoded file and...
531
+ #
532
+ # encryption: {
533
+ # method: :simple_tls,
534
+ # tls_options: { ca_file: '/path/to/my-little-ca.pem',
535
+ # ssl_version: 'TLSv1_1' },
536
+ # }
537
+ #
538
+ # As you might guess, the above example also fails the connection
539
+ # if the client can't negotiate TLS v1.1.
540
+ # tls_options is ultimately passed to OpenSSL::SSL::SSLContext#set_params
541
+ # For more details, see
542
+ # http://ruby-doc.org/stdlib-2.0.0/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html
473
543
  #
474
544
  # Instantiating a Net::LDAP object does <i>not</i> result in network
475
545
  # traffic to the LDAP server. It simply stores the connection and binding
476
- # parameters in the object.
546
+ # parameters in the object. That's why Net::LDAP.new doesn't throw
547
+ # cert validation errors itself; #bind does instead.
477
548
  def initialize(args = {})
478
549
  @host = args[:host] || DefaultHost
479
550
  @port = args[:port] || DefaultPort
@@ -482,7 +553,8 @@ class Net::LDAP
482
553
  @auth = args[:auth] || DefaultAuth
483
554
  @base = args[:base] || DefaultTreebase
484
555
  @force_no_page = args[:force_no_page] || DefaultForceNoPage
485
- encryption args[:encryption] # may be nil
556
+ @encryption = normalize_encryption(args[:encryption]) # may be nil
557
+ @connect_timeout = args[:connect_timeout]
486
558
 
487
559
  if pr = @auth[:password] and pr.respond_to?(:call)
488
560
  @auth[:password] = pr.call
@@ -533,7 +605,7 @@ class Net::LDAP
533
605
  @auth = {
534
606
  :method => :simple,
535
607
  :username => username,
536
- :password => password
608
+ :password => password,
537
609
  }
538
610
  end
539
611
  alias_method :auth, :authenticate
@@ -546,54 +618,12 @@ class Net::LDAP
546
618
  # additional capabilities are added, more configuration values will be
547
619
  # added here.
548
620
  #
549
- # The :simple_tls encryption method encrypts <i>all</i> communications
550
- # with the LDAP server. It completely establishes SSL/TLS encryption with
551
- # the LDAP server before any LDAP-protocol data is exchanged. There is no
552
- # plaintext negotiation and no special encryption-request controls are
553
- # sent to the server. <i>The :simple_tls option is the simplest, easiest
554
- # way to encrypt communications between Net::LDAP and LDAP servers.</i>
555
- # It's intended for cases where you have an implicit level of trust in the
556
- # authenticity of the LDAP server. No validation of the LDAP server's SSL
557
- # certificate is performed. This means that :simple_tls will not produce
558
- # errors if the LDAP server's encryption certificate is not signed by a
559
- # well-known Certification Authority. If you get communications or
560
- # protocol errors when using this option, check with your LDAP server
561
- # administrator. Pay particular attention to the TCP port you are
562
- # connecting to. It's impossible for an LDAP server to support plaintext
563
- # LDAP communications and <i>simple TLS</i> connections on the same port.
564
- # The standard TCP port for unencrypted LDAP connections is 389, but the
565
- # standard port for simple-TLS encrypted connections is 636. Be sure you
566
- # are using the correct port.
567
- #
568
- # The :start_tls like the :simple_tls encryption method also encrypts all
569
- # communcations with the LDAP server. With the exception that it operates
570
- # over the standard TCP port.
571
- #
572
- # In order to verify certificates and enable other TLS options, the
573
- # :tls_options hash can be passed alongside :simple_tls or :start_tls.
574
- # This hash contains any options that can be passed to
575
- # OpenSSL::SSL::SSLContext#set_params(). The most common options passed
576
- # should be OpenSSL::SSL::SSLContext::DEFAULT_PARAMS, or the :ca_file option,
577
- # which contains a path to a Certificate Authority file (PEM-encoded).
578
- #
579
- # Example for a default setup without custom settings:
580
- # {
581
- # :method => :simple_tls,
582
- # :tls_options => OpenSSL::SSL::SSLContext::DEFAULT_PARAMS
583
- # }
584
- #
585
- # Example for specifying a CA-File and only allowing TLSv1.1 connections:
621
+ # This method is deprecated.
586
622
  #
587
- # {
588
- # :method => :start_tls,
589
- # :tls_options => { :ca_file => "/etc/cafile.pem", :ssl_version => "TLSv1_1" }
590
- # }
591
623
  def encryption(args)
592
- case args
593
- when :simple_tls, :start_tls
594
- args = { :method => args, :tls_options => {} }
595
- end
596
- @encryption = args
624
+ warn "Deprecation warning: please give :encryption option as a Hash to Net::LDAP.new"
625
+ return if args.nil?
626
+ @encryption = normalize_encryption(args)
597
627
  end
598
628
 
599
629
  # #open takes the same parameters as #new. #open makes a network
@@ -637,8 +667,11 @@ class Net::LDAP
637
667
  #++
638
668
  def get_operation_result
639
669
  result = @result
640
- result = result.result if result.is_a?(Net::LDAP::PDU)
641
670
  os = OpenStruct.new
671
+ if result.is_a?(Net::LDAP::PDU)
672
+ os.extended_response = result.extended_response
673
+ result = result.result
674
+ end
642
675
  if result.is_a?(Hash)
643
676
  # We might get a hash of LDAP response codes instead of a simple
644
677
  # numeric code.
@@ -681,7 +714,7 @@ class Net::LDAP
681
714
  begin
682
715
  @open_connection = new_connection
683
716
  payload[:connection] = @open_connection
684
- payload[:bind] = @open_connection.bind(@auth)
717
+ payload[:bind] = @result = @open_connection.bind(@auth)
685
718
  yield self
686
719
  ensure
687
720
  @open_connection.close if @open_connection
@@ -750,10 +783,10 @@ class Net::LDAP
750
783
 
751
784
  instrument "search.net_ldap", args do |payload|
752
785
  @result = use_connection(args) do |conn|
753
- conn.search(args) { |entry|
786
+ conn.search(args) do |entry|
754
787
  result_set << entry if result_set
755
788
  yield entry if block_given?
756
- }
789
+ end
757
790
  end
758
791
 
759
792
  if return_result_set
@@ -892,7 +925,7 @@ class Net::LDAP
892
925
  # end
893
926
  def bind_as(args = {})
894
927
  result = false
895
- open { |me|
928
+ open do |me|
896
929
  rs = search args
897
930
  if rs and rs.first and dn = rs.first.dn
898
931
  password = args[:password]
@@ -900,7 +933,7 @@ class Net::LDAP
900
933
  result = rs if bind(:method => :simple, :username => dn,
901
934
  :password => password)
902
935
  end
903
- }
936
+ end
904
937
  result
905
938
  end
906
939
 
@@ -1027,6 +1060,44 @@ class Net::LDAP
1027
1060
  end
1028
1061
  end
1029
1062
 
1063
+ # Password Modify
1064
+ #
1065
+ # Change existing password:
1066
+ #
1067
+ # dn = 'uid=modify-password-user1,ou=People,dc=rubyldap,dc=com'
1068
+ # auth = {
1069
+ # method: :simple,
1070
+ # username: dn,
1071
+ # password: 'passworD1'
1072
+ # }
1073
+ # ldap.password_modify(dn: dn,
1074
+ # auth: auth,
1075
+ # old_password: 'passworD1',
1076
+ # new_password: 'passworD2')
1077
+ #
1078
+ # Or get the LDAP server to generate a password for you:
1079
+ #
1080
+ # dn = 'uid=modify-password-user1,ou=People,dc=rubyldap,dc=com'
1081
+ # auth = {
1082
+ # method: :simple,
1083
+ # username: dn,
1084
+ # password: 'passworD1'
1085
+ # }
1086
+ # ldap.password_modify(dn: dn,
1087
+ # auth: auth,
1088
+ # old_password: 'passworD1')
1089
+ #
1090
+ # ldap.get_operation_result.extended_response[0][0] #=> 'VtcgGf/G'
1091
+ #
1092
+ def password_modify(args)
1093
+ instrument "modify_password.net_ldap", args do |payload|
1094
+ @result = use_connection(args) do |conn|
1095
+ conn.password_modify(args)
1096
+ end
1097
+ @result.success?
1098
+ end
1099
+ end
1100
+
1030
1101
  # Add a value to an attribute. Takes the full DN of the entry to modify,
1031
1102
  # the name (Symbol or String) of the attribute, and the value (String or
1032
1103
  # Array). If the attribute does not exist (and there are no schema
@@ -1113,14 +1184,22 @@ class Net::LDAP
1113
1184
  # entries. This method sends an extra control code to tell the LDAP server
1114
1185
  # to do a tree delete. ('1.2.840.113556.1.4.805')
1115
1186
  #
1187
+ # If the LDAP server does not support the DELETE_TREE control code, subordinate
1188
+ # entries are deleted recursively instead.
1189
+ #
1116
1190
  # Returns True or False to indicate whether the delete succeeded. Extended
1117
1191
  # status information is available by calling #get_operation_result.
1118
1192
  #
1119
1193
  # dn = "mail=deleteme@example.com, ou=people, dc=example, dc=com"
1120
1194
  # ldap.delete_tree :dn => dn
1121
1195
  def delete_tree(args)
1122
- delete(args.merge(:control_codes => [[Net::LDAP::LDAPControls::DELETE_TREE, true]]))
1196
+ if search_root_dse[:supportedcontrol].include? Net::LDAP::LDAPControls::DELETE_TREE
1197
+ delete(args.merge(:control_codes => [[Net::LDAP::LDAPControls::DELETE_TREE, true]]))
1198
+ else
1199
+ recursive_delete(args)
1200
+ end
1123
1201
  end
1202
+
1124
1203
  # This method is experimental and subject to change. Return the rootDSE
1125
1204
  # record from the LDAP server as a Net::LDAP::Entry, or an empty Entry if
1126
1205
  # the server doesn't return the record.
@@ -1145,7 +1224,7 @@ class Net::LDAP
1145
1224
  :supportedExtension,
1146
1225
  :supportedFeatures,
1147
1226
  :supportedLdapVersion,
1148
- :supportedSASLMechanisms
1227
+ :supportedSASLMechanisms,
1149
1228
  ])
1150
1229
  (rs and rs.first) or Net::LDAP::Entry.new
1151
1230
  end
@@ -1212,6 +1291,11 @@ class Net::LDAP
1212
1291
  inspected
1213
1292
  end
1214
1293
 
1294
+ # Internal: Set @open_connection for testing
1295
+ def connection=(connection)
1296
+ @open_connection = connection
1297
+ end
1298
+
1215
1299
  private
1216
1300
 
1217
1301
  # Yields an open connection if there is one, otherwise establishes a new
@@ -1224,11 +1308,9 @@ class Net::LDAP
1224
1308
  else
1225
1309
  begin
1226
1310
  conn = new_connection
1227
- if (result = conn.bind(args[:auth] || @auth)).result_code == Net::LDAP::ResultCodeSuccess
1228
- yield conn
1229
- else
1230
- return result
1231
- end
1311
+ result = conn.bind(args[:auth] || @auth)
1312
+ return result unless result.result_code == Net::LDAP::ResultCodeSuccess
1313
+ yield conn
1232
1314
  ensure
1233
1315
  conn.close if conn
1234
1316
  end
@@ -1237,11 +1319,50 @@ class Net::LDAP
1237
1319
 
1238
1320
  # Establish a new connection to the LDAP server
1239
1321
  def new_connection
1240
- Net::LDAP::Connection.new \
1322
+ connection = Net::LDAP::Connection.new \
1241
1323
  :host => @host,
1242
1324
  :port => @port,
1243
1325
  :hosts => @hosts,
1244
1326
  :encryption => @encryption,
1245
- :instrumentation_service => @instrumentation_service
1327
+ :instrumentation_service => @instrumentation_service,
1328
+ :connect_timeout => @connect_timeout
1329
+
1330
+ # Force connect to see if there's a connection error
1331
+ connection.socket
1332
+ connection
1333
+ rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
1334
+ @result = {
1335
+ :resultCode => 52,
1336
+ :errorMessage => ResultStrings[ResultCodeUnavailable],
1337
+ }
1338
+ raise e
1339
+ end
1340
+
1341
+ # Normalize encryption parameter the constructor accepts, expands a few
1342
+ # convenience symbols into recognizable hashes
1343
+ def normalize_encryption(args)
1344
+ return if args.nil?
1345
+ return args if args.is_a? Hash
1346
+
1347
+ case method = args.to_sym
1348
+ when :simple_tls, :start_tls
1349
+ { :method => method, :tls_options => {} }
1350
+ end
1351
+ end
1352
+
1353
+ # Recursively delete a dn and it's subordinate children.
1354
+ # This is useful when a server does not support the DELETE_TREE control code.
1355
+ def recursive_delete(args)
1356
+ raise EmptyDNError unless args.is_a?(Hash) && args.key?(:dn)
1357
+ # Delete Children
1358
+ search(base: args[:dn], scope: Net::LDAP::SearchScope_SingleLevel) do |entry|
1359
+ recursive_delete(dn: entry.dn)
1360
+ end
1361
+ # Delete Self
1362
+ unless delete(dn: args[:dn])
1363
+ raise Net::LDAP::Error, get_operation_result[:error_message].to_s
1364
+ end
1365
+ true
1246
1366
  end
1367
+
1247
1368
  end # class LDAP