rubinius-net-ldap 0.11

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 (73) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rubocop.yml +5 -0
  4. data/.rubocop_todo.yml +462 -0
  5. data/.travis.yml +19 -0
  6. data/CONTRIBUTING.md +54 -0
  7. data/Contributors.rdoc +24 -0
  8. data/Gemfile +2 -0
  9. data/Hacking.rdoc +63 -0
  10. data/History.rdoc +260 -0
  11. data/License.rdoc +29 -0
  12. data/README.rdoc +65 -0
  13. data/Rakefile +17 -0
  14. data/lib/net-ldap.rb +2 -0
  15. data/lib/net/ber.rb +320 -0
  16. data/lib/net/ber/ber_parser.rb +182 -0
  17. data/lib/net/ber/core_ext.rb +55 -0
  18. data/lib/net/ber/core_ext/array.rb +96 -0
  19. data/lib/net/ber/core_ext/false_class.rb +10 -0
  20. data/lib/net/ber/core_ext/integer.rb +74 -0
  21. data/lib/net/ber/core_ext/string.rb +66 -0
  22. data/lib/net/ber/core_ext/true_class.rb +11 -0
  23. data/lib/net/ldap.rb +1229 -0
  24. data/lib/net/ldap/connection.rb +702 -0
  25. data/lib/net/ldap/dataset.rb +168 -0
  26. data/lib/net/ldap/dn.rb +225 -0
  27. data/lib/net/ldap/entry.rb +193 -0
  28. data/lib/net/ldap/error.rb +38 -0
  29. data/lib/net/ldap/filter.rb +778 -0
  30. data/lib/net/ldap/instrumentation.rb +23 -0
  31. data/lib/net/ldap/password.rb +38 -0
  32. data/lib/net/ldap/pdu.rb +297 -0
  33. data/lib/net/ldap/version.rb +5 -0
  34. data/lib/net/snmp.rb +264 -0
  35. data/rubinius-net-ldap.gemspec +37 -0
  36. data/script/install-openldap +112 -0
  37. data/script/package +7 -0
  38. data/script/release +16 -0
  39. data/test/ber/core_ext/test_array.rb +22 -0
  40. data/test/ber/core_ext/test_string.rb +25 -0
  41. data/test/ber/test_ber.rb +99 -0
  42. data/test/fixtures/cacert.pem +20 -0
  43. data/test/fixtures/openldap/memberof.ldif +33 -0
  44. data/test/fixtures/openldap/retcode.ldif +76 -0
  45. data/test/fixtures/openldap/slapd.conf.ldif +67 -0
  46. data/test/fixtures/seed.ldif +374 -0
  47. data/test/integration/test_add.rb +28 -0
  48. data/test/integration/test_ber.rb +30 -0
  49. data/test/integration/test_bind.rb +34 -0
  50. data/test/integration/test_delete.rb +31 -0
  51. data/test/integration/test_open.rb +88 -0
  52. data/test/integration/test_return_codes.rb +38 -0
  53. data/test/integration/test_search.rb +77 -0
  54. data/test/support/vm/openldap/.gitignore +1 -0
  55. data/test/support/vm/openldap/README.md +32 -0
  56. data/test/support/vm/openldap/Vagrantfile +33 -0
  57. data/test/test_dn.rb +44 -0
  58. data/test/test_entry.rb +65 -0
  59. data/test/test_filter.rb +223 -0
  60. data/test/test_filter_parser.rb +20 -0
  61. data/test/test_helper.rb +66 -0
  62. data/test/test_ldap.rb +60 -0
  63. data/test/test_ldap_connection.rb +404 -0
  64. data/test/test_ldif.rb +104 -0
  65. data/test/test_password.rb +10 -0
  66. data/test/test_rename.rb +77 -0
  67. data/test/test_search.rb +39 -0
  68. data/test/test_snmp.rb +119 -0
  69. data/test/test_ssl_ber.rb +40 -0
  70. data/test/testdata.ldif +101 -0
  71. data/testserver/ldapserver.rb +210 -0
  72. data/testserver/testdata.ldif +101 -0
  73. metadata +204 -0
@@ -0,0 +1,55 @@
1
+ # -*- ruby encoding: utf-8 -*-
2
+ require 'net/ber/ber_parser'
3
+ # :stopdoc:
4
+ class IO
5
+ include Net::BER::BERParser
6
+ end
7
+
8
+ class StringIO
9
+ include Net::BER::BERParser
10
+ end
11
+
12
+ if defined? ::OpenSSL
13
+ class OpenSSL::SSL::SSLSocket
14
+ include Net::BER::BERParser
15
+ end
16
+ end
17
+ # :startdoc:
18
+
19
+ module Net::BER::Extensions # :nodoc:
20
+ end
21
+
22
+ require 'net/ber/core_ext/string'
23
+ # :stopdoc:
24
+ class String
25
+ include Net::BER::BERParser
26
+ include Net::BER::Extensions::String
27
+ end
28
+
29
+ require 'net/ber/core_ext/array'
30
+ # :stopdoc:
31
+ class Array
32
+ include Net::BER::Extensions::Array
33
+ end
34
+ # :startdoc:
35
+
36
+ require 'net/ber/core_ext/integer'
37
+ # :stopdoc:
38
+ class Integer
39
+ include Net::BER::Extensions::Integer
40
+ end
41
+ # :startdoc:
42
+
43
+ require 'net/ber/core_ext/true_class'
44
+ # :stopdoc:
45
+ class TrueClass
46
+ include Net::BER::Extensions::TrueClass
47
+ end
48
+ # :startdoc:
49
+
50
+ require 'net/ber/core_ext/false_class'
51
+ # :stopdoc:
52
+ class FalseClass
53
+ include Net::BER::Extensions::FalseClass
54
+ end
55
+ # :startdoc:
@@ -0,0 +1,96 @@
1
+ # -*- ruby encoding: utf-8 -*-
2
+ ##
3
+ # BER extensions to the Array class.
4
+ module Net::BER::Extensions::Array
5
+ ##
6
+ # Converts an Array to a BER sequence. All values in the Array are
7
+ # expected to be in BER format prior to calling this method.
8
+ def to_ber(id = 0)
9
+ # The universal sequence tag 0x30 is composed of the base tag value
10
+ # (0x10) and the constructed flag (0x20).
11
+ to_ber_seq_internal(0x30 + id)
12
+ end
13
+ alias_method :to_ber_sequence, :to_ber
14
+
15
+ ##
16
+ # Converts an Array to a BER set. All values in the Array are expected to
17
+ # be in BER format prior to calling this method.
18
+ def to_ber_set(id = 0)
19
+ # The universal set tag 0x31 is composed of the base tag value (0x11)
20
+ # and the constructed flag (0x20).
21
+ to_ber_seq_internal(0x31 + id)
22
+ end
23
+
24
+ ##
25
+ # Converts an Array to an application-specific sequence, assigned a tag
26
+ # value that is meaningful to the particular protocol being used. All
27
+ # values in the Array are expected to be in BER format pr prior to calling
28
+ # this method.
29
+ #--
30
+ # Implementor's note 20100320(AZ): RFC 4511 (the LDAPv3 protocol) as well
31
+ # as earlier RFCs 1777 and 2559 seem to indicate that LDAP only has
32
+ # application constructed sequences (0x60). However, ldapsearch sends some
33
+ # context-specific constructed sequences (0xA0); other clients may do the
34
+ # same. This behaviour appears to violate the RFCs. In real-world
35
+ # practice, we may need to change calls of #to_ber_appsequence to
36
+ # #to_ber_contextspecific for full LDAP server compatibility.
37
+ #
38
+ # This note probably belongs elsewhere.
39
+ #++
40
+ def to_ber_appsequence(id = 0)
41
+ # The application sequence tag always starts from the application flag
42
+ # (0x40) and the constructed flag (0x20).
43
+ to_ber_seq_internal(0x60 + id)
44
+ end
45
+
46
+ ##
47
+ # Converts an Array to a context-specific sequence, assigned a tag value
48
+ # that is meaningful to the particular context of the particular protocol
49
+ # being used. All values in the Array are expected to be in BER format
50
+ # prior to calling this method.
51
+ def to_ber_contextspecific(id = 0)
52
+ # The application sequence tag always starts from the context flag
53
+ # (0x80) and the constructed flag (0x20).
54
+ to_ber_seq_internal(0xa0 + id)
55
+ end
56
+
57
+ ##
58
+ # The internal sequence packing routine. All values in the Array are
59
+ # expected to be in BER format prior to calling this method.
60
+ def to_ber_seq_internal(code)
61
+ s = self.join
62
+ [code].pack('C') + s.length.to_ber_length_encoding + s
63
+ end
64
+ private :to_ber_seq_internal
65
+
66
+ ##
67
+ # SNMP Object Identifiers (OID) are special arrays
68
+ #--
69
+ # 20100320 AZ: I do not think that this method should be in BER, since
70
+ # this appears to be SNMP-specific. This should probably be subsumed by a
71
+ # proper SNMP OID object.
72
+ #++
73
+ def to_ber_oid
74
+ ary = self.dup
75
+ first = ary.shift
76
+ raise Net::BER::BerError, "Invalid OID" unless [0, 1, 2].include?(first)
77
+ first = first * 40 + ary.shift
78
+ ary.unshift first
79
+ oid = ary.pack("w*")
80
+ [6, oid.length].pack("CC") + oid
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{|element| element.to_ber}.to_ber_sequence.reject_empty_ber_arrays
93
+ end
94
+ ary.to_ber_sequence.reject_empty_ber_arrays
95
+ end
96
+ end
@@ -0,0 +1,10 @@
1
+ # -*- ruby encoding: utf-8 -*-
2
+ ##
3
+ # BER extensions to +false+.
4
+ module Net::BER::Extensions::FalseClass
5
+ ##
6
+ # Converts +false+ to the BER wireline representation of +false+.
7
+ def to_ber
8
+ "\001\001\000"
9
+ end
10
+ 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
@@ -0,0 +1,66 @@
1
+ # -*- ruby encoding: utf-8 -*-
2
+ require 'stringio'
3
+
4
+ ##
5
+ # BER extensions to the String class.
6
+ module Net::BER::Extensions::String
7
+ ##
8
+ # Converts a string to a BER string. Universal octet-strings are tagged
9
+ # with 0x04, but other values are possible depending on the context, so we
10
+ # let the caller give us one.
11
+ #
12
+ # User code should call either #to_ber_application_string or
13
+ # #to_ber_contextspecific.
14
+ def to_ber(code = 0x04)
15
+ raw_string = raw_utf8_encoded
16
+ [code].pack('C') + raw_string.length.to_ber_length_encoding + raw_string
17
+ end
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
+
27
+ def raw_utf8_encoded
28
+ self
29
+ end
30
+ private :raw_utf8_encoded
31
+
32
+ ##
33
+ # Creates an application-specific BER string encoded value with the
34
+ # provided syntax code value.
35
+ def to_ber_application_string(code)
36
+ to_ber(0x40 + code)
37
+ end
38
+
39
+ ##
40
+ # Creates a context-specific BER string encoded value with the provided
41
+ # syntax code value.
42
+ def to_ber_contextspecific(code)
43
+ to_ber(0x80 + code)
44
+ end
45
+
46
+ ##
47
+ # Nondestructively reads a BER object from this string.
48
+ def read_ber(syntax = nil)
49
+ StringIO.new(self).read_ber(syntax)
50
+ end
51
+
52
+ ##
53
+ # Destructively reads a BER object from the string.
54
+ def read_ber!(syntax = nil)
55
+ io = StringIO.new(self)
56
+
57
+ result = io.read_ber(syntax)
58
+ self.slice!(0...io.pos)
59
+
60
+ return result
61
+ end
62
+
63
+ def reject_empty_ber_arrays
64
+ self.gsub(/0\000/n,'')
65
+ end
66
+ end
@@ -0,0 +1,11 @@
1
+ # -*- ruby encoding: utf-8 -*-
2
+ ##
3
+ # BER extensions to +true+.
4
+ module Net::BER::Extensions::TrueClass
5
+ ##
6
+ # Converts +true+ to the BER wireline representation of +true+.
7
+ def to_ber
8
+ # http://tools.ietf.org/html/rfc4511#section-5.1
9
+ "\001\001\xFF"
10
+ end
11
+ end
data/lib/net/ldap.rb ADDED
@@ -0,0 +1,1229 @@
1
+ # -*- ruby encoding: utf-8 -*-
2
+ require 'ostruct'
3
+
4
+ module Net # :nodoc:
5
+ class LDAP
6
+ begin
7
+ require 'openssl'
8
+ ##
9
+ # Set to +true+ if OpenSSL is available and LDAPS is supported.
10
+ HasOpenSSL = true
11
+ rescue LoadError
12
+ # :stopdoc:
13
+ HasOpenSSL = false
14
+ # :startdoc:
15
+ end
16
+ end
17
+ end
18
+ require 'socket'
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
+
31
+ # == Quick-start for the Impatient
32
+ # === Quick Example of a user-authentication against an LDAP directory:
33
+ #
34
+ # require 'rubygems'
35
+ # require 'net/ldap'
36
+ #
37
+ # ldap = Net::LDAP.new
38
+ # ldap.host = your_server_ip_address
39
+ # ldap.port = 389
40
+ # ldap.auth "joe_user", "opensesame"
41
+ # if ldap.bind
42
+ # # authentication succeeded
43
+ # else
44
+ # # authentication failed
45
+ # end
46
+ #
47
+ #
48
+ # === Quick Example of a search against an LDAP directory:
49
+ #
50
+ # require 'rubygems'
51
+ # require 'net/ldap'
52
+ #
53
+ # ldap = Net::LDAP.new :host => server_ip_address,
54
+ # :port => 389,
55
+ # :auth => {
56
+ # :method => :simple,
57
+ # :username => "cn=manager, dc=example, dc=com",
58
+ # :password => "opensesame"
59
+ # }
60
+ #
61
+ # filter = Net::LDAP::Filter.eq("cn", "George*")
62
+ # treebase = "dc=example, dc=com"
63
+ #
64
+ # ldap.search(:base => treebase, :filter => filter) do |entry|
65
+ # puts "DN: #{entry.dn}"
66
+ # entry.each do |attribute, values|
67
+ # puts " #{attribute}:"
68
+ # values.each do |value|
69
+ # puts " --->#{value}"
70
+ # end
71
+ # end
72
+ # end
73
+ #
74
+ # p ldap.get_operation_result
75
+ #
76
+ #
77
+ # == A Brief Introduction to LDAP
78
+ #
79
+ # We're going to provide a quick, informal introduction to LDAP terminology
80
+ # and typical operations. If you're comfortable with this material, skip
81
+ # ahead to "How to use Net::LDAP." If you want a more rigorous treatment of
82
+ # this material, we recommend you start with the various IETF and ITU
83
+ # standards that relate to LDAP.
84
+ #
85
+ # === Entities
86
+ # LDAP is an Internet-standard protocol used to access directory servers.
87
+ # The basic search unit is the <i>entity, </i> which corresponds to a person
88
+ # or other domain-specific object. A directory service which supports the
89
+ # LDAP protocol typically stores information about a number of entities.
90
+ #
91
+ # === Principals
92
+ # LDAP servers are typically used to access information about people, but
93
+ # also very often about such items as printers, computers, and other
94
+ # resources. To reflect this, LDAP uses the term <i>entity, </i> or less
95
+ # commonly, <i>principal, </i> to denote its basic data-storage unit.
96
+ #
97
+ # === Distinguished Names
98
+ # In LDAP's view of the world, an entity is uniquely identified by a
99
+ # globally-unique text string called a <i>Distinguished Name, </i> originally
100
+ # defined in the X.400 standards from which LDAP is ultimately derived. Much
101
+ # like a DNS hostname, a DN is a "flattened" text representation of a string
102
+ # of tree nodes. Also like DNS (and unlike Java package names), a DN
103
+ # expresses a chain of tree-nodes written from left to right in order from
104
+ # the most-resolved node to the most-general one.
105
+ #
106
+ # If you know the DN of a person or other entity, then you can query an
107
+ # LDAP-enabled directory for information (attributes) about the entity.
108
+ # Alternatively, you can query the directory for a list of DNs matching a
109
+ # set of criteria that you supply.
110
+ #
111
+ # === Attributes
112
+ #
113
+ # In the LDAP view of the world, a DN uniquely identifies an entity.
114
+ # Information about the entity is stored as a set of <i>Attributes.</i> An
115
+ # attribute is a text string which is associated with zero or more values.
116
+ # Most LDAP-enabled directories store a well-standardized range of
117
+ # attributes, and constrain their values according to standard rules.
118
+ #
119
+ # A good example of an attribute is <tt>sn, </tt> which stands for "Surname."
120
+ # This attribute is generally used to store a person's surname, or last
121
+ # name. Most directories enforce the standard convention that an entity's
122
+ # <tt>sn</tt> attribute have <i>exactly one</i> value. In LDAP jargon, that
123
+ # means that <tt>sn</tt> must be <i>present</i> and <i>single-valued.</i>
124
+ #
125
+ # Another attribute is <tt>mail, </tt> which is used to store email
126
+ # addresses. (No, there is no attribute called "email, " perhaps because
127
+ # X.400 terminology predates the invention of the term <i>email.</i>)
128
+ # <tt>mail</tt> differs from <tt>sn</tt> in that most directories permit any
129
+ # number of values for the <tt>mail</tt> attribute, including zero.
130
+ #
131
+ # === Tree-Base
132
+ # We said above that X.400 Distinguished Names are <i>globally unique.</i>
133
+ # In a manner reminiscent of DNS, LDAP supposes that each directory server
134
+ # contains authoritative attribute data for a set of DNs corresponding to a
135
+ # specific sub-tree of the (notional) global directory tree. This subtree is
136
+ # generally configured into a directory server when it is created. It
137
+ # matters for this discussion because most servers will not allow you to
138
+ # query them unless you specify a correct tree-base.
139
+ #
140
+ # Let's say you work for the engineering department of Big Company, Inc.,
141
+ # whose internet domain is bigcompany.com. You may find that your
142
+ # departmental directory is stored in a server with a defined tree-base of
143
+ # ou=engineering, dc=bigcompany, dc=com
144
+ # You will need to supply this string as the <i>tree-base</i> when querying
145
+ # this directory. (Ou is a very old X.400 term meaning "organizational
146
+ # unit." Dc is a more recent term meaning "domain component.")
147
+ #
148
+ # === LDAP Versions
149
+ # (stub, discuss v2 and v3)
150
+ #
151
+ # === LDAP Operations
152
+ # The essential operations are: #bind, #search, #add, #modify, #delete, and
153
+ # #rename.
154
+ #
155
+ # ==== Bind
156
+ # #bind supplies a user's authentication credentials to a server, which in
157
+ # turn verifies or rejects them. There is a range of possibilities for
158
+ # credentials, but most directories support a simple username and password
159
+ # authentication.
160
+ #
161
+ # Taken by itself, #bind can be used to authenticate a user against
162
+ # information stored in a directory, for example to permit or deny access to
163
+ # some other resource. In terms of the other LDAP operations, most
164
+ # directories require a successful #bind to be performed before the other
165
+ # operations will be permitted. Some servers permit certain operations to be
166
+ # performed with an "anonymous" binding, meaning that no credentials are
167
+ # presented by the user. (We're glossing over a lot of platform-specific
168
+ # detail here.)
169
+ #
170
+ # ==== Search
171
+ # Calling #search against the directory involves specifying a treebase, a
172
+ # set of <i>search filters, </i> and a list of attribute values. The filters
173
+ # specify ranges of possible values for particular attributes. Multiple
174
+ # filters can be joined together with AND, OR, and NOT operators. A server
175
+ # will respond to a #search by returning a list of matching DNs together
176
+ # with a set of attribute values for each entity, depending on what
177
+ # attributes the search requested.
178
+ #
179
+ # ==== Add
180
+ # #add specifies a new DN and an initial set of attribute values. If the
181
+ # operation succeeds, a new entity with the corresponding DN and attributes
182
+ # is added to the directory.
183
+ #
184
+ # ==== Modify
185
+ # #modify specifies an entity DN, and a list of attribute operations.
186
+ # #modify is used to change the attribute values stored in the directory for
187
+ # a particular entity. #modify may add or delete attributes (which are lists
188
+ # of values) or it change attributes by adding to or deleting from their
189
+ # values. Net::LDAP provides three easier methods to modify an entry's
190
+ # attribute values: #add_attribute, #replace_attribute, and
191
+ # #delete_attribute.
192
+ #
193
+ # ==== Delete
194
+ # #delete specifies an entity DN. If it succeeds, the entity and all its
195
+ # attributes is removed from the directory.
196
+ #
197
+ # ==== Rename (or Modify RDN)
198
+ # #rename (or #modify_rdn) is an operation added to version 3 of the LDAP
199
+ # protocol. It responds to the often-arising need to change the DN of an
200
+ # entity without discarding its attribute values. In earlier LDAP versions,
201
+ # the only way to do this was to delete the whole entity and add it again
202
+ # with a different DN.
203
+ #
204
+ # #rename works by taking an "old" DN (the one to change) and a "new RDN, "
205
+ # which is the left-most part of the DN string. If successful, #rename
206
+ # changes the entity DN so that its left-most node corresponds to the new
207
+ # RDN given in the request. (RDN, or "relative distinguished name, " denotes
208
+ # a single tree-node as expressed in a DN, which is a chain of tree nodes.)
209
+ #
210
+ # == How to use Net::LDAP
211
+ # To access Net::LDAP functionality in your Ruby programs, start by
212
+ # requiring the library:
213
+ #
214
+ # require 'net/ldap'
215
+ #
216
+ # If you installed the Gem version of Net::LDAP, and depending on your
217
+ # version of Ruby and rubygems, you _may_ also need to require rubygems
218
+ # explicitly:
219
+ #
220
+ # require 'rubygems'
221
+ # require 'net/ldap'
222
+ #
223
+ # Most operations with Net::LDAP start by instantiating a Net::LDAP object.
224
+ # The constructor for this object takes arguments specifying the network
225
+ # location (address and port) of the LDAP server, and also the binding
226
+ # (authentication) credentials, typically a username and password. Given an
227
+ # object of class Net:LDAP, you can then perform LDAP operations by calling
228
+ # instance methods on the object. These are documented with usage examples
229
+ # below.
230
+ #
231
+ # The Net::LDAP library is designed to be very disciplined about how it
232
+ # makes network connections to servers. This is different from many of the
233
+ # standard native-code libraries that are provided on most platforms, which
234
+ # share bloodlines with the original Netscape/Michigan LDAP client
235
+ # implementations. These libraries sought to insulate user code from the
236
+ # workings of the network. This is a good idea of course, but the practical
237
+ # effect has been confusing and many difficult bugs have been caused by the
238
+ # opacity of the native libraries, and their variable behavior across
239
+ # platforms.
240
+ #
241
+ # In general, Net::LDAP instance methods which invoke server operations make
242
+ # a connection to the server when the method is called. They execute the
243
+ # operation (typically binding first) and then disconnect from the server.
244
+ # The exception is Net::LDAP#open, which makes a connection to the server
245
+ # and then keeps it open while it executes a user-supplied block.
246
+ # Net::LDAP#open closes the connection on completion of the block.
247
+ class Net::LDAP
248
+ include Net::LDAP::Instrumentation
249
+
250
+ SearchScope_BaseObject = 0
251
+ SearchScope_SingleLevel = 1
252
+ SearchScope_WholeSubtree = 2
253
+ SearchScopes = [ SearchScope_BaseObject, SearchScope_SingleLevel,
254
+ SearchScope_WholeSubtree ]
255
+
256
+ DerefAliases_Never = 0
257
+ DerefAliases_Search = 1
258
+ DerefAliases_Find = 2
259
+ DerefAliases_Always = 3
260
+ DerefAliasesArray = [ DerefAliases_Never, DerefAliases_Search, DerefAliases_Find, DerefAliases_Always ]
261
+
262
+ primitive = { 2 => :null } # UnbindRequest body
263
+ constructed = {
264
+ 0 => :array, # BindRequest
265
+ 1 => :array, # BindResponse
266
+ 2 => :array, # UnbindRequest
267
+ 3 => :array, # SearchRequest
268
+ 4 => :array, # SearchData
269
+ 5 => :array, # SearchResult
270
+ 6 => :array, # ModifyRequest
271
+ 7 => :array, # ModifyResponse
272
+ 8 => :array, # AddRequest
273
+ 9 => :array, # AddResponse
274
+ 10 => :array, # DelRequest
275
+ 11 => :array, # DelResponse
276
+ 12 => :array, # ModifyRdnRequest
277
+ 13 => :array, # ModifyRdnResponse
278
+ 14 => :array, # CompareRequest
279
+ 15 => :array, # CompareResponse
280
+ 16 => :array, # AbandonRequest
281
+ 19 => :array, # SearchResultReferral
282
+ 24 => :array, # Unsolicited Notification
283
+ }
284
+ application = {
285
+ :primitive => primitive,
286
+ :constructed => constructed,
287
+ }
288
+ primitive = {
289
+ 0 => :string, # password
290
+ 1 => :string, # Kerberos v4
291
+ 2 => :string, # Kerberos v5
292
+ 3 => :string, # SearchFilter-extensible
293
+ 4 => :string, # SearchFilter-extensible
294
+ 7 => :string, # serverSaslCreds
295
+ }
296
+ constructed = {
297
+ 0 => :array, # RFC-2251 Control and Filter-AND
298
+ 1 => :array, # SearchFilter-OR
299
+ 2 => :array, # SearchFilter-NOT
300
+ 3 => :array, # Seach referral
301
+ 4 => :array, # unknown use in Microsoft Outlook
302
+ 5 => :array, # SearchFilter-GE
303
+ 6 => :array, # SearchFilter-LE
304
+ 7 => :array, # serverSaslCreds
305
+ 9 => :array, # SearchFilter-extensible
306
+ }
307
+ context_specific = {
308
+ :primitive => primitive,
309
+ :constructed => constructed,
310
+ }
311
+
312
+ AsnSyntax = Net::BER.compile_syntax(:application => application,
313
+ :context_specific => context_specific)
314
+
315
+ DefaultHost = "127.0.0.1"
316
+ DefaultPort = 389
317
+ DefaultAuth = { :method => :anonymous }
318
+ DefaultTreebase = "dc=com"
319
+ DefaultForceNoPage = false
320
+
321
+ StartTlsOid = "1.3.6.1.4.1.1466.20037"
322
+
323
+ # https://tools.ietf.org/html/rfc4511#section-4.1.9
324
+ # https://tools.ietf.org/html/rfc4511#appendix-A
325
+ ResultCodeSuccess = 0
326
+ ResultCodeOperationsError = 1
327
+ ResultCodeProtocolError = 2
328
+ ResultCodeTimeLimitExceeded = 3
329
+ ResultCodeSizeLimitExceeded = 4
330
+ ResultCodeCompareFalse = 5
331
+ ResultCodeCompareTrue = 6
332
+ ResultCodeAuthMethodNotSupported = 7
333
+ ResultCodeStrongerAuthRequired = 8
334
+ ResultCodeReferral = 10
335
+ ResultCodeAdminLimitExceeded = 11
336
+ ResultCodeUnavailableCriticalExtension = 12
337
+ ResultCodeConfidentialityRequired = 13
338
+ ResultCodeSaslBindInProgress = 14
339
+ ResultCodeNoSuchAttribute = 16
340
+ ResultCodeUndefinedAttributeType = 17
341
+ ResultCodeInappropriateMatching = 18
342
+ ResultCodeConstraintViolation = 19
343
+ ResultCodeAttributeOrValueExists = 20
344
+ ResultCodeInvalidAttributeSyntax = 21
345
+ ResultCodeNoSuchObject = 32
346
+ ResultCodeAliasProblem = 33
347
+ ResultCodeInvalidDNSyntax = 34
348
+ ResultCodeAliasDereferencingProblem = 36
349
+ ResultCodeInappropriateAuthentication = 48
350
+ ResultCodeInvalidCredentials = 49
351
+ ResultCodeInsufficientAccessRights = 50
352
+ ResultCodeBusy = 51
353
+ ResultCodeUnavailable = 52
354
+ ResultCodeUnwillingToPerform = 53
355
+ ResultCodeNamingViolation = 64
356
+ ResultCodeObjectClassViolation = 65
357
+ ResultCodeNotAllowedOnNonLeaf = 66
358
+ ResultCodeNotAllowedOnRDN = 67
359
+ ResultCodeEntryAlreadyExists = 68
360
+ ResultCodeObjectClassModsProhibited = 69
361
+ ResultCodeAffectsMultipleDSAs = 71
362
+ ResultCodeOther = 80
363
+
364
+ # https://tools.ietf.org/html/rfc4511#appendix-A.1
365
+ ResultCodesNonError = [
366
+ ResultCodeSuccess,
367
+ ResultCodeCompareFalse,
368
+ ResultCodeCompareTrue,
369
+ ResultCodeReferral,
370
+ ResultCodeSaslBindInProgress
371
+ ]
372
+
373
+ # nonstandard list of "successful" result codes for searches
374
+ ResultCodesSearchSuccess = [
375
+ ResultCodeSuccess,
376
+ ResultCodeTimeLimitExceeded,
377
+ ResultCodeSizeLimitExceeded
378
+ ]
379
+
380
+ # map of result code to human message
381
+ ResultStrings = {
382
+ ResultCodeSuccess => "Success",
383
+ ResultCodeOperationsError => "Operations Error",
384
+ ResultCodeProtocolError => "Protocol Error",
385
+ ResultCodeTimeLimitExceeded => "Time Limit Exceeded",
386
+ ResultCodeSizeLimitExceeded => "Size Limit Exceeded",
387
+ ResultCodeCompareFalse => "False Comparison",
388
+ ResultCodeCompareTrue => "True Comparison",
389
+ ResultCodeAuthMethodNotSupported => "Auth Method Not Supported",
390
+ ResultCodeStrongerAuthRequired => "Stronger Auth Needed",
391
+ ResultCodeReferral => "Referral",
392
+ ResultCodeAdminLimitExceeded => "Admin Limit Exceeded",
393
+ ResultCodeUnavailableCriticalExtension => "Unavailable crtical extension",
394
+ ResultCodeConfidentialityRequired => "Confidentiality Required",
395
+ ResultCodeSaslBindInProgress => "saslBindInProgress",
396
+ ResultCodeNoSuchAttribute => "No Such Attribute",
397
+ ResultCodeUndefinedAttributeType => "Undefined Attribute Type",
398
+ ResultCodeInappropriateMatching => "Inappropriate Matching",
399
+ ResultCodeConstraintViolation => "Constraint Violation",
400
+ ResultCodeAttributeOrValueExists => "Attribute or Value Exists",
401
+ ResultCodeInvalidAttributeSyntax => "Invalide Attribute Syntax",
402
+ ResultCodeNoSuchObject => "No Such Object",
403
+ ResultCodeAliasProblem => "Alias Problem",
404
+ ResultCodeInvalidDNSyntax => "Invalid DN Syntax",
405
+ ResultCodeAliasDereferencingProblem => "Alias Dereferencing Problem",
406
+ ResultCodeInappropriateAuthentication => "Inappropriate Authentication",
407
+ ResultCodeInvalidCredentials => "Invalid Credentials",
408
+ ResultCodeInsufficientAccessRights => "Insufficient Access Rights",
409
+ ResultCodeBusy => "Busy",
410
+ ResultCodeUnavailable => "Unavailable",
411
+ ResultCodeUnwillingToPerform => "Unwilling to perform",
412
+ ResultCodeNamingViolation => "Naming Violation",
413
+ ResultCodeObjectClassViolation => "Object Class Violation",
414
+ ResultCodeNotAllowedOnNonLeaf => "Not Allowed On Non-Leaf",
415
+ ResultCodeNotAllowedOnRDN => "Not Allowed On RDN",
416
+ ResultCodeEntryAlreadyExists => "Entry Already Exists",
417
+ ResultCodeObjectClassModsProhibited => "ObjectClass Modifications Prohibited",
418
+ ResultCodeAffectsMultipleDSAs => "Affects Multiple DSAs",
419
+ ResultCodeOther => "Other"
420
+ }
421
+
422
+ module LDAPControls
423
+ PAGED_RESULTS = "1.2.840.113556.1.4.319" # Microsoft evil from RFC 2696
424
+ SORT_REQUEST = "1.2.840.113556.1.4.473"
425
+ SORT_RESPONSE = "1.2.840.113556.1.4.474"
426
+ DELETE_TREE = "1.2.840.113556.1.4.805"
427
+ end
428
+
429
+ def self.result2string(code) #:nodoc:
430
+ ResultStrings[code] || "unknown result (#{code})"
431
+ end
432
+
433
+ attr_accessor :host
434
+ attr_accessor :port
435
+ attr_accessor :base
436
+
437
+ # Instantiate an object of type Net::LDAP to perform directory operations.
438
+ # This constructor takes a Hash containing arguments, all of which are
439
+ # either optional or may be specified later with other methods as
440
+ # described below. The following arguments are supported:
441
+ # * :host => the LDAP server's IP-address (default 127.0.0.1)
442
+ # * :port => the LDAP server's TCP port (default 389)
443
+ # * :auth => a Hash containing authorization parameters. Currently
444
+ # supported values include: {:method => :anonymous} and {:method =>
445
+ # :simple, :username => your_user_name, :password => your_password }
446
+ # The password parameter may be a Proc that returns a String.
447
+ # * :base => a default treebase parameter for searches performed against
448
+ # the LDAP server. If you don't give this value, then each call to
449
+ # #search must specify a treebase parameter. If you do give this value,
450
+ # then it will be used in subsequent calls to #search that do not
451
+ # specify a treebase. If you give a treebase value in any particular
452
+ # call to #search, that value will override any treebase value you give
453
+ # here.
454
+ # * :encryption => specifies the encryption to be used in communicating
455
+ # with the LDAP server. The value is either a Hash containing additional
456
+ # parameters, or the Symbol :simple_tls, which is equivalent to
457
+ # specifying the Hash {:method => :simple_tls}. There is a fairly large
458
+ # range of potential values that may be given for this parameter. See
459
+ # #encryption for details.
460
+ # * :force_no_page => Set to true to prevent paged results even if your
461
+ # server says it supports them. This is a fix for MS Active Directory
462
+ # * :instrumentation_service => An object responsible for instrumenting
463
+ # operations, compatible with ActiveSupport::Notifications' public API.
464
+ #
465
+ # Instantiating a Net::LDAP object does <i>not</i> result in network
466
+ # traffic to the LDAP server. It simply stores the connection and binding
467
+ # parameters in the object.
468
+ def initialize(args = {})
469
+ @host = args[:host] || DefaultHost
470
+ @port = args[:port] || DefaultPort
471
+ @verbose = false # Make this configurable with a switch on the class.
472
+ @auth = args[:auth] || DefaultAuth
473
+ @base = args[:base] || DefaultTreebase
474
+ @force_no_page = args[:force_no_page] || DefaultForceNoPage
475
+ encryption args[:encryption] # may be nil
476
+
477
+ if pr = @auth[:password] and pr.respond_to?(:call)
478
+ @auth[:password] = pr.call
479
+ end
480
+
481
+ @instrumentation_service = args[:instrumentation_service]
482
+
483
+ # This variable is only set when we are created with LDAP::open. All of
484
+ # our internal methods will connect using it, or else they will create
485
+ # their own.
486
+ @open_connection = nil
487
+ end
488
+
489
+ # Convenience method to specify authentication credentials to the LDAP
490
+ # server. Currently supports simple authentication requiring a username
491
+ # and password.
492
+ #
493
+ # Observe that on most LDAP servers, the username is a complete DN.
494
+ # However, with A/D, it's often possible to give only a user-name rather
495
+ # than a complete DN. In the latter case, beware that many A/D servers are
496
+ # configured to permit anonymous (uncredentialled) binding, and will
497
+ # silently accept your binding as anonymous if you give an unrecognized
498
+ # username. This is not usually what you want. (See
499
+ # #get_operation_result.)
500
+ #
501
+ # <b>Important:</b> The password argument may be a Proc that returns a
502
+ # string. This makes it possible for you to write client programs that
503
+ # solicit passwords from users or from other data sources without showing
504
+ # them in your code or on command lines.
505
+ #
506
+ # require 'net/ldap'
507
+ #
508
+ # ldap = Net::LDAP.new
509
+ # ldap.host = server_ip_address
510
+ # ldap.authenticate "cn=Your Username, cn=Users, dc=example, dc=com", "your_psw"
511
+ #
512
+ # Alternatively (with a password block):
513
+ #
514
+ # require 'net/ldap'
515
+ #
516
+ # ldap = Net::LDAP.new
517
+ # ldap.host = server_ip_address
518
+ # psw = proc { your_psw_function }
519
+ # ldap.authenticate "cn=Your Username, cn=Users, dc=example, dc=com", psw
520
+ #
521
+ def authenticate(username, password)
522
+ password = password.call if password.respond_to?(:call)
523
+ @auth = {
524
+ :method => :simple,
525
+ :username => username,
526
+ :password => password
527
+ }
528
+ end
529
+ alias_method :auth, :authenticate
530
+
531
+ # Convenience method to specify encryption characteristics for connections
532
+ # to LDAP servers. Called implicitly by #new and #open, but may also be
533
+ # called by user code if desired. The single argument is generally a Hash
534
+ # (but see below for convenience alternatives). This implementation is
535
+ # currently a stub, supporting only a few encryption alternatives. As
536
+ # additional capabilities are added, more configuration values will be
537
+ # added here.
538
+ #
539
+ # The :simple_tls encryption method encrypts <i>all</i> communications
540
+ # with the LDAP server. It completely establishes SSL/TLS encryption with
541
+ # the LDAP server before any LDAP-protocol data is exchanged. There is no
542
+ # plaintext negotiation and no special encryption-request controls are
543
+ # sent to the server. <i>The :simple_tls option is the simplest, easiest
544
+ # way to encrypt communications between Net::LDAP and LDAP servers.</i>
545
+ # It's intended for cases where you have an implicit level of trust in the
546
+ # authenticity of the LDAP server. No validation of the LDAP server's SSL
547
+ # certificate is performed. This means that :simple_tls will not produce
548
+ # errors if the LDAP server's encryption certificate is not signed by a
549
+ # well-known Certification Authority. If you get communications or
550
+ # protocol errors when using this option, check with your LDAP server
551
+ # administrator. Pay particular attention to the TCP port you are
552
+ # connecting to. It's impossible for an LDAP server to support plaintext
553
+ # LDAP communications and <i>simple TLS</i> connections on the same port.
554
+ # The standard TCP port for unencrypted LDAP connections is 389, but the
555
+ # standard port for simple-TLS encrypted connections is 636. Be sure you
556
+ # are using the correct port.
557
+ #
558
+ # The :start_tls like the :simple_tls encryption method also encrypts all
559
+ # communcations with the LDAP server. With the exception that it operates
560
+ # over the standard TCP port.
561
+ #
562
+ # In order to verify certificates and enable other TLS options, the
563
+ # :tls_options hash can be passed alongside :simple_tls or :start_tls.
564
+ # This hash contains any options that can be passed to
565
+ # OpenSSL::SSL::SSLContext#set_params(). The most common options passed
566
+ # should be OpenSSL::SSL::SSLContext::DEFAULT_PARAMS, or the :ca_file option,
567
+ # which contains a path to a Certificate Authority file (PEM-encoded).
568
+ #
569
+ # Example for a default setup without custom settings:
570
+ # {
571
+ # :method => :simple_tls,
572
+ # :tls_options => OpenSSL::SSL::SSLContext::DEFAULT_PARAMS
573
+ # }
574
+ #
575
+ # Example for specifying a CA-File and only allowing TLSv1.1 connections:
576
+ #
577
+ # {
578
+ # :method => :start_tls,
579
+ # :tls_options => { :ca_file => "/etc/cafile.pem", :ssl_version => "TLSv1_1" }
580
+ # }
581
+ def encryption(args)
582
+ case args
583
+ when :simple_tls, :start_tls
584
+ args = { :method => args, :tls_options => {} }
585
+ end
586
+ @encryption = args
587
+ end
588
+
589
+ # #open takes the same parameters as #new. #open makes a network
590
+ # connection to the LDAP server and then passes a newly-created Net::LDAP
591
+ # object to the caller-supplied block. Within the block, you can call any
592
+ # of the instance methods of Net::LDAP to perform operations against the
593
+ # LDAP directory. #open will perform all the operations in the
594
+ # user-supplied block on the same network connection, which will be closed
595
+ # automatically when the block finishes.
596
+ #
597
+ # # (PSEUDOCODE)
598
+ # auth = { :method => :simple, :username => username, :password => password }
599
+ # Net::LDAP.open(:host => ipaddress, :port => 389, :auth => auth) do |ldap|
600
+ # ldap.search(...)
601
+ # ldap.add(...)
602
+ # ldap.modify(...)
603
+ # end
604
+ def self.open(args)
605
+ ldap1 = new(args)
606
+ ldap1.open { |ldap| yield ldap }
607
+ end
608
+
609
+ # Returns a meaningful result any time after a protocol operation (#bind,
610
+ # #search, #add, #modify, #rename, #delete) has completed. It returns an
611
+ # #OpenStruct containing an LDAP result code (0 means success), and a
612
+ # human-readable string.
613
+ #
614
+ # unless ldap.bind
615
+ # puts "Result: #{ldap.get_operation_result.code}"
616
+ # puts "Message: #{ldap.get_operation_result.message}"
617
+ # end
618
+ #
619
+ # Certain operations return additional information, accessible through
620
+ # members of the object returned from #get_operation_result. Check
621
+ # #get_operation_result.error_message and
622
+ # #get_operation_result.matched_dn.
623
+ #
624
+ #--
625
+ # Modified the implementation, 20Mar07. We might get a hash of LDAP
626
+ # response codes instead of a simple numeric code.
627
+ #++
628
+ def get_operation_result
629
+ result = @result
630
+ result = result.result if result.is_a?(Net::LDAP::PDU)
631
+ os = OpenStruct.new
632
+ if result.is_a?(Hash)
633
+ # We might get a hash of LDAP response codes instead of a simple
634
+ # numeric code.
635
+ os.code = (result[:resultCode] || "").to_i
636
+ os.error_message = result[:errorMessage]
637
+ os.matched_dn = result[:matchedDN]
638
+ elsif result
639
+ os.code = result
640
+ else
641
+ os.code = Net::LDAP::ResultCodeSuccess
642
+ end
643
+ os.message = Net::LDAP.result2string(os.code)
644
+ os
645
+ end
646
+
647
+ # Opens a network connection to the server and then passes <tt>self</tt>
648
+ # to the caller-supplied block. The connection is closed when the block
649
+ # completes. Used for executing multiple LDAP operations without requiring
650
+ # a separate network connection (and authentication) for each one.
651
+ # <i>Note:</i> You do not need to log-in or "bind" to the server. This
652
+ # will be done for you automatically. For an even simpler approach, see
653
+ # the class method Net::LDAP#open.
654
+ #
655
+ # # (PSEUDOCODE)
656
+ # auth = { :method => :simple, :username => username, :password => password }
657
+ # ldap = Net::LDAP.new(:host => ipaddress, :port => 389, :auth => auth)
658
+ # ldap.open do |ldap|
659
+ # ldap.search(...)
660
+ # ldap.add(...)
661
+ # ldap.modify(...)
662
+ # end
663
+ def open
664
+ # First we make a connection and then a binding, but we don't do
665
+ # anything with the bind results. We then pass self to the caller's
666
+ # block, where he will execute his LDAP operations. Of course they will
667
+ # all generate auth failures if the bind was unsuccessful.
668
+ raise Net::LDAP::AlreadyOpenedError, "Open already in progress" if @open_connection
669
+
670
+ instrument "open.net_ldap" do |payload|
671
+ begin
672
+ @open_connection = new_connection
673
+ payload[:connection] = @open_connection
674
+ payload[:bind] = @open_connection.bind(@auth)
675
+ yield self
676
+ ensure
677
+ @open_connection.close if @open_connection
678
+ @open_connection = nil
679
+ end
680
+ end
681
+ end
682
+
683
+ # Searches the LDAP directory for directory entries. Takes a hash argument
684
+ # with parameters. Supported parameters include:
685
+ # * :base (a string specifying the tree-base for the search);
686
+ # * :filter (an object of type Net::LDAP::Filter, defaults to
687
+ # objectclass=*);
688
+ # * :attributes (a string or array of strings specifying the LDAP
689
+ # attributes to return from the server);
690
+ # * :return_result (a boolean specifying whether to return a result set).
691
+ # * :attributes_only (a boolean flag, defaults false)
692
+ # * :scope (one of: Net::LDAP::SearchScope_BaseObject,
693
+ # Net::LDAP::SearchScope_SingleLevel,
694
+ # Net::LDAP::SearchScope_WholeSubtree. Default is WholeSubtree.)
695
+ # * :size (an integer indicating the maximum number of search entries to
696
+ # return. Default is zero, which signifies no limit.)
697
+ # * :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)
698
+ # * :deref (one of: Net::LDAP::DerefAliases_Never, Net::LDAP::DerefAliases_Search,
699
+ # Net::LDAP::DerefAliases_Find, Net::LDAP::DerefAliases_Always. Default is Never.)
700
+ #
701
+ # #search queries the LDAP server and passes <i>each entry</i> to the
702
+ # caller-supplied block, as an object of type Net::LDAP::Entry. If the
703
+ # search returns 1000 entries, the block will be called 1000 times. If the
704
+ # search returns no entries, the block will not be called.
705
+ #
706
+ # #search returns either a result-set or a boolean, depending on the value
707
+ # of the <tt>:return_result</tt> argument. The default behavior is to
708
+ # return a result set, which is an Array of objects of class
709
+ # Net::LDAP::Entry. If you request a result set and #search fails with an
710
+ # error, it will return nil. Call #get_operation_result to get the error
711
+ # information returned by
712
+ # the LDAP server.
713
+ #
714
+ # When <tt>:return_result => false, </tt> #search will return only a
715
+ # Boolean, to indicate whether the operation succeeded. This can improve
716
+ # performance with very large result sets, because the library can discard
717
+ # each entry from memory after your block processes it.
718
+ #
719
+ # treebase = "dc=example, dc=com"
720
+ # filter = Net::LDAP::Filter.eq("mail", "a*.com")
721
+ # attrs = ["mail", "cn", "sn", "objectclass"]
722
+ # ldap.search(:base => treebase, :filter => filter, :attributes => attrs,
723
+ # :return_result => false) do |entry|
724
+ # puts "DN: #{entry.dn}"
725
+ # entry.each do |attr, values|
726
+ # puts ".......#{attr}:"
727
+ # values.each do |value|
728
+ # puts " #{value}"
729
+ # end
730
+ # end
731
+ # end
732
+ def search(args = {})
733
+ unless args[:ignore_server_caps]
734
+ args[:paged_searches_supported] = paged_searches_supported?
735
+ end
736
+
737
+ args[:base] ||= @base
738
+ return_result_set = args[:return_result] != false
739
+ result_set = return_result_set ? [] : nil
740
+
741
+ instrument "search.net_ldap", args do |payload|
742
+ @result = use_connection(args) do |conn|
743
+ conn.search(args) { |entry|
744
+ result_set << entry if result_set
745
+ yield entry if block_given?
746
+ }
747
+ end
748
+
749
+ if return_result_set
750
+ unless @result.nil?
751
+ if ResultCodesSearchSuccess.include?(@result.result_code)
752
+ result_set
753
+ end
754
+ end
755
+ else
756
+ @result.success?
757
+ end
758
+ end
759
+ end
760
+
761
+ # #bind connects to an LDAP server and requests authentication based on
762
+ # the <tt>:auth</tt> parameter passed to #open or #new. It takes no
763
+ # parameters.
764
+ #
765
+ # User code does not need to call #bind directly. It will be called
766
+ # implicitly by the library whenever you invoke an LDAP operation, such as
767
+ # #search or #add.
768
+ #
769
+ # It is useful, however, to call #bind in your own code when the only
770
+ # operation you intend to perform against the directory is to validate a
771
+ # login credential. #bind returns true or false to indicate whether the
772
+ # binding was successful. Reasons for failure include malformed or
773
+ # unrecognized usernames and incorrect passwords. Use
774
+ # #get_operation_result to find out what happened in case of failure.
775
+ #
776
+ # Here's a typical example using #bind to authenticate a credential which
777
+ # was (perhaps) solicited from the user of a web site:
778
+ #
779
+ # require 'net/ldap'
780
+ # ldap = Net::LDAP.new
781
+ # ldap.host = your_server_ip_address
782
+ # ldap.port = 389
783
+ # ldap.auth your_user_name, your_user_password
784
+ # if ldap.bind
785
+ # # authentication succeeded
786
+ # else
787
+ # # authentication failed
788
+ # p ldap.get_operation_result
789
+ # end
790
+ #
791
+ # Here's a more succinct example which does exactly the same thing, but
792
+ # collects all the required parameters into arguments:
793
+ #
794
+ # require 'net/ldap'
795
+ # ldap = Net::LDAP.new(:host => your_server_ip_address, :port => 389)
796
+ # if ldap.bind(:method => :simple, :username => your_user_name,
797
+ # :password => your_user_password)
798
+ # # authentication succeeded
799
+ # else
800
+ # # authentication failed
801
+ # p ldap.get_operation_result
802
+ # end
803
+ #
804
+ # You don't need to pass a user-password as a String object to bind. You
805
+ # can also pass a Ruby Proc object which returns a string. This will cause
806
+ # bind to execute the Proc (which might then solicit input from a user
807
+ # with console display suppressed). The String value returned from the
808
+ # Proc is used as the password.
809
+ #
810
+ # You don't have to create a new instance of Net::LDAP every time you
811
+ # perform a binding in this way. If you prefer, you can cache the
812
+ # Net::LDAP object and re-use it to perform subsequent bindings,
813
+ # <i>provided</i> you call #auth to specify a new credential before
814
+ # calling #bind. Otherwise, you'll just re-authenticate the previous user!
815
+ # (You don't need to re-set the values of #host and #port.) As noted in
816
+ # the documentation for #auth, the password parameter can be a Ruby Proc
817
+ # instead of a String.
818
+ def bind(auth = @auth)
819
+ instrument "bind.net_ldap" do |payload|
820
+ if @open_connection
821
+ payload[:connection] = @open_connection
822
+ payload[:bind] = @result = @open_connection.bind(auth)
823
+ else
824
+ begin
825
+ conn = new_connection
826
+ payload[:connection] = conn
827
+ payload[:bind] = @result = conn.bind(auth)
828
+ ensure
829
+ conn.close if conn
830
+ end
831
+ end
832
+
833
+ @result.success?
834
+ end
835
+ end
836
+
837
+ # #bind_as is for testing authentication credentials.
838
+ #
839
+ # As described under #bind, most LDAP servers require that you supply a
840
+ # complete DN as a binding-credential, along with an authenticator such as
841
+ # a password. But for many applications (such as authenticating users to a
842
+ # Rails application), you often don't have a full DN to identify the user.
843
+ # You usually get a simple identifier like a username or an email address,
844
+ # along with a password. #bind_as allows you to authenticate these
845
+ # user-identifiers.
846
+ #
847
+ # #bind_as is a combination of a search and an LDAP binding. First, it
848
+ # connects and binds to the directory as normal. Then it searches the
849
+ # directory for an entry corresponding to the email address, username, or
850
+ # other string that you supply. If the entry exists, then #bind_as will
851
+ # <b>re-bind</b> as that user with the password (or other authenticator)
852
+ # that you supply.
853
+ #
854
+ # #bind_as takes the same parameters as #search, <i>with the addition of
855
+ # an authenticator.</i> Currently, this authenticator must be
856
+ # <tt>:password</tt>. Its value may be either a String, or a +proc+ that
857
+ # returns a String. #bind_as returns +false+ on failure. On success, it
858
+ # returns a result set, just as #search does. This result set is an Array
859
+ # of objects of type Net::LDAP::Entry. It contains the directory
860
+ # attributes corresponding to the user. (Just test whether the return
861
+ # value is logically true, if you don't need this additional information.)
862
+ #
863
+ # Here's how you would use #bind_as to authenticate an email address and
864
+ # password:
865
+ #
866
+ # require 'net/ldap'
867
+ #
868
+ # user, psw = "joe_user@yourcompany.com", "joes_psw"
869
+ #
870
+ # ldap = Net::LDAP.new
871
+ # ldap.host = "192.168.0.100"
872
+ # ldap.port = 389
873
+ # ldap.auth "cn=manager, dc=yourcompany, dc=com", "topsecret"
874
+ #
875
+ # result = ldap.bind_as(:base => "dc=yourcompany, dc=com",
876
+ # :filter => "(mail=#{user})",
877
+ # :password => psw)
878
+ # if result
879
+ # puts "Authenticated #{result.first.dn}"
880
+ # else
881
+ # puts "Authentication FAILED."
882
+ # end
883
+ def bind_as(args = {})
884
+ result = false
885
+ open { |me|
886
+ rs = search args
887
+ if rs and rs.first and dn = rs.first.dn
888
+ password = args[:password]
889
+ password = password.call if password.respond_to?(:call)
890
+ result = rs if bind(:method => :simple, :username => dn,
891
+ :password => password)
892
+ end
893
+ }
894
+ result
895
+ end
896
+
897
+ # Adds a new entry to the remote LDAP server.
898
+ # Supported arguments:
899
+ # :dn :: Full DN of the new entry
900
+ # :attributes :: Attributes of the new entry.
901
+ #
902
+ # The attributes argument is supplied as a Hash keyed by Strings or
903
+ # Symbols giving the attribute name, and mapping to Strings or Arrays of
904
+ # Strings giving the actual attribute values. Observe that most LDAP
905
+ # directories enforce schema constraints on the attributes contained in
906
+ # entries. #add will fail with a server-generated error if your attributes
907
+ # violate the server-specific constraints.
908
+ #
909
+ # Here's an example:
910
+ #
911
+ # dn = "cn=George Smith, ou=people, dc=example, dc=com"
912
+ # attr = {
913
+ # :cn => "George Smith",
914
+ # :objectclass => ["top", "inetorgperson"],
915
+ # :sn => "Smith",
916
+ # :mail => "gsmith@example.com"
917
+ # }
918
+ # Net::LDAP.open(:host => host) do |ldap|
919
+ # ldap.add(:dn => dn, :attributes => attr)
920
+ # end
921
+ def add(args)
922
+ instrument "add.net_ldap", args do |payload|
923
+ @result = use_connection(args) do |conn|
924
+ conn.add(args)
925
+ end
926
+ @result.success?
927
+ end
928
+ end
929
+
930
+ # Modifies the attribute values of a particular entry on the LDAP
931
+ # directory. Takes a hash with arguments. Supported arguments are:
932
+ # :dn :: (the full DN of the entry whose attributes are to be modified)
933
+ # :operations :: (the modifications to be performed, detailed next)
934
+ #
935
+ # This method returns True or False to indicate whether the operation
936
+ # succeeded or failed, with extended information available by calling
937
+ # #get_operation_result.
938
+ #
939
+ # Also see #add_attribute, #replace_attribute, or #delete_attribute, which
940
+ # provide simpler interfaces to this functionality.
941
+ #
942
+ # The LDAP protocol provides a full and well thought-out set of operations
943
+ # for changing the values of attributes, but they are necessarily somewhat
944
+ # complex and not always intuitive. If these instructions are confusing or
945
+ # incomplete, please send us email or create an issue on GitHub.
946
+ #
947
+ # The :operations parameter to #modify takes an array of
948
+ # operation-descriptors. Each individual operation is specified in one
949
+ # element of the array, and most LDAP servers will attempt to perform the
950
+ # operations in order.
951
+ #
952
+ # Each of the operations appearing in the Array must itself be an Array
953
+ # with exactly three elements:
954
+ # an operator :: must be :add, :replace, or :delete
955
+ # an attribute name :: the attribute name (string or symbol) to modify
956
+ # a value :: either a string or an array of strings.
957
+ #
958
+ # The :add operator will, unsurprisingly, add the specified values to the
959
+ # specified attribute. If the attribute does not already exist, :add will
960
+ # create it. Most LDAP servers will generate an error if you try to add a
961
+ # value that already exists.
962
+ #
963
+ # :replace will erase the current value(s) for the specified attribute, if
964
+ # there are any, and replace them with the specified value(s).
965
+ #
966
+ # :delete will remove the specified value(s) from the specified attribute.
967
+ # If you pass nil, an empty string, or an empty array as the value
968
+ # parameter to a :delete operation, the _entire_ _attribute_ will be
969
+ # deleted, along with all of its values.
970
+ #
971
+ # For example:
972
+ #
973
+ # dn = "mail=modifyme@example.com, ou=people, dc=example, dc=com"
974
+ # ops = [
975
+ # [:add, :mail, "aliasaddress@example.com"],
976
+ # [:replace, :mail, ["newaddress@example.com", "newalias@example.com"]],
977
+ # [:delete, :sn, nil]
978
+ # ]
979
+ # ldap.modify :dn => dn, :operations => ops
980
+ #
981
+ # <i>(This example is contrived since you probably wouldn't add a mail
982
+ # value right before replacing the whole attribute, but it shows that
983
+ # order of execution matters. Also, many LDAP servers won't let you delete
984
+ # SN because that would be a schema violation.)</i>
985
+ #
986
+ # It's essential to keep in mind that if you specify more than one
987
+ # operation in a call to #modify, most LDAP servers will attempt to
988
+ # perform all of the operations in the order you gave them. This matters
989
+ # because you may specify operations on the same attribute which must be
990
+ # performed in a certain order.
991
+ #
992
+ # Most LDAP servers will _stop_ processing your modifications if one of
993
+ # them causes an error on the server (such as a schema-constraint
994
+ # violation). If this happens, you will probably get a result code from
995
+ # the server that reflects only the operation that failed, and you may or
996
+ # may not get extended information that will tell you which one failed.
997
+ # #modify has no notion of an atomic transaction. If you specify a chain
998
+ # of modifications in one call to #modify, and one of them fails, the
999
+ # preceding ones will usually not be "rolled back", resulting in a
1000
+ # partial update. This is a limitation of the LDAP protocol, not of
1001
+ # Net::LDAP.
1002
+ #
1003
+ # The lack of transactional atomicity in LDAP means that you're usually
1004
+ # better off using the convenience methods #add_attribute,
1005
+ # #replace_attribute, and #delete_attribute, which are wrappers over
1006
+ # #modify. However, certain LDAP servers may provide concurrency
1007
+ # semantics, in which the several operations contained in a single #modify
1008
+ # call are not interleaved with other modification-requests received
1009
+ # simultaneously by the server. It bears repeating that this concurrency
1010
+ # does _not_ imply transactional atomicity, which LDAP does not provide.
1011
+ def modify(args)
1012
+ instrument "modify.net_ldap", args do |payload|
1013
+ @result = use_connection(args) do |conn|
1014
+ conn.modify(args)
1015
+ end
1016
+ @result.success?
1017
+ end
1018
+ end
1019
+
1020
+ # Add a value to an attribute. Takes the full DN of the entry to modify,
1021
+ # the name (Symbol or String) of the attribute, and the value (String or
1022
+ # Array). If the attribute does not exist (and there are no schema
1023
+ # violations), #add_attribute will create it with the caller-specified
1024
+ # values. If the attribute already exists (and there are no schema
1025
+ # violations), the caller-specified values will be _added_ to the values
1026
+ # already present.
1027
+ #
1028
+ # Returns True or False to indicate whether the operation succeeded or
1029
+ # failed, with extended information available by calling
1030
+ # #get_operation_result. See also #replace_attribute and
1031
+ # #delete_attribute.
1032
+ #
1033
+ # dn = "cn=modifyme, dc=example, dc=com"
1034
+ # ldap.add_attribute dn, :mail, "newmailaddress@example.com"
1035
+ def add_attribute(dn, attribute, value)
1036
+ modify(:dn => dn, :operations => [[:add, attribute, value]])
1037
+ end
1038
+
1039
+ # Replace the value of an attribute. #replace_attribute can be thought of
1040
+ # as equivalent to calling #delete_attribute followed by #add_attribute.
1041
+ # It takes the full DN of the entry to modify, the name (Symbol or String)
1042
+ # of the attribute, and the value (String or Array). If the attribute does
1043
+ # not exist, it will be created with the caller-specified value(s). If the
1044
+ # attribute does exist, its values will be _discarded_ and replaced with
1045
+ # the caller-specified values.
1046
+ #
1047
+ # Returns True or False to indicate whether the operation succeeded or
1048
+ # failed, with extended information available by calling
1049
+ # #get_operation_result. See also #add_attribute and #delete_attribute.
1050
+ #
1051
+ # dn = "cn=modifyme, dc=example, dc=com"
1052
+ # ldap.replace_attribute dn, :mail, "newmailaddress@example.com"
1053
+ def replace_attribute(dn, attribute, value)
1054
+ modify(:dn => dn, :operations => [[:replace, attribute, value]])
1055
+ end
1056
+
1057
+ # Delete an attribute and all its values. Takes the full DN of the entry
1058
+ # to modify, and the name (Symbol or String) of the attribute to delete.
1059
+ #
1060
+ # Returns True or False to indicate whether the operation succeeded or
1061
+ # failed, with extended information available by calling
1062
+ # #get_operation_result. See also #add_attribute and #replace_attribute.
1063
+ #
1064
+ # dn = "cn=modifyme, dc=example, dc=com"
1065
+ # ldap.delete_attribute dn, :mail
1066
+ def delete_attribute(dn, attribute)
1067
+ modify(:dn => dn, :operations => [[:delete, attribute, nil]])
1068
+ end
1069
+
1070
+ # Rename an entry on the remote DIS by changing the last RDN of its DN.
1071
+ #
1072
+ # _Documentation_ _stub_
1073
+ def rename(args)
1074
+ instrument "rename.net_ldap", args do |payload|
1075
+ @result = use_connection(args) do |conn|
1076
+ conn.rename(args)
1077
+ end
1078
+ @result.success?
1079
+ end
1080
+ end
1081
+ alias_method :modify_rdn, :rename
1082
+
1083
+ # Delete an entry from the LDAP directory. Takes a hash of arguments. The
1084
+ # only supported argument is :dn, which must give the complete DN of the
1085
+ # entry to be deleted.
1086
+ #
1087
+ # Returns True or False to indicate whether the delete succeeded. Extended
1088
+ # status information is available by calling #get_operation_result.
1089
+ #
1090
+ # dn = "mail=deleteme@example.com, ou=people, dc=example, dc=com"
1091
+ # ldap.delete :dn => dn
1092
+ def delete(args)
1093
+ instrument "delete.net_ldap", args do |payload|
1094
+ @result = use_connection(args) do |conn|
1095
+ conn.delete(args)
1096
+ end
1097
+ @result.success?
1098
+ end
1099
+ end
1100
+
1101
+ # Delete an entry from the LDAP directory along with all subordinate entries.
1102
+ # the regular delete method will fail to delete an entry if it has subordinate
1103
+ # entries. This method sends an extra control code to tell the LDAP server
1104
+ # to do a tree delete. ('1.2.840.113556.1.4.805')
1105
+ #
1106
+ # Returns True or False to indicate whether the delete succeeded. Extended
1107
+ # status information is available by calling #get_operation_result.
1108
+ #
1109
+ # dn = "mail=deleteme@example.com, ou=people, dc=example, dc=com"
1110
+ # ldap.delete_tree :dn => dn
1111
+ def delete_tree(args)
1112
+ delete(args.merge(:control_codes => [[Net::LDAP::LDAPControls::DELETE_TREE, true]]))
1113
+ end
1114
+ # This method is experimental and subject to change. Return the rootDSE
1115
+ # record from the LDAP server as a Net::LDAP::Entry, or an empty Entry if
1116
+ # the server doesn't return the record.
1117
+ #--
1118
+ # cf. RFC4512 graf 5.1.
1119
+ # Note that the rootDSE record we return on success has an empty DN, which
1120
+ # is correct. On failure, the empty Entry will have a nil DN. There's no
1121
+ # real reason for that, so it can be changed if desired. The funky
1122
+ # number-disagreements in the set of attribute names is correct per the
1123
+ # RFC. We may be called by #search itself, which may need to determine
1124
+ # things like paged search capabilities. So to avoid an infinite regress,
1125
+ # set :ignore_server_caps, which prevents us getting called recursively.
1126
+ #++
1127
+ def search_root_dse
1128
+ rs = search(:ignore_server_caps => true, :base => "",
1129
+ :scope => SearchScope_BaseObject,
1130
+ :attributes => [
1131
+ :altServer,
1132
+ :namingContexts,
1133
+ :supportedCapabilities,
1134
+ :supportedControl,
1135
+ :supportedExtension,
1136
+ :supportedFeatures,
1137
+ :supportedLdapVersion,
1138
+ :supportedSASLMechanisms
1139
+ ])
1140
+ (rs and rs.first) or Net::LDAP::Entry.new
1141
+ end
1142
+
1143
+ # Return the root Subschema record from the LDAP server as a
1144
+ # Net::LDAP::Entry, or an empty Entry if the server doesn't return the
1145
+ # record. On success, the Net::LDAP::Entry returned from this call will
1146
+ # have the attributes :dn, :objectclasses, and :attributetypes. If there
1147
+ # is an error, call #get_operation_result for more information.
1148
+ #
1149
+ # ldap = Net::LDAP.new
1150
+ # ldap.host = "your.ldap.host"
1151
+ # ldap.auth "your-user-dn", "your-psw"
1152
+ # subschema_entry = ldap.search_subschema_entry
1153
+ #
1154
+ # subschema_entry.attributetypes.each do |attrtype|
1155
+ # # your code
1156
+ # end
1157
+ #
1158
+ # subschema_entry.objectclasses.each do |attrtype|
1159
+ # # your code
1160
+ # end
1161
+ #--
1162
+ # cf. RFC4512 section 4, particulary graff 4.4.
1163
+ # The :dn attribute in the returned Entry is the subschema name as
1164
+ # returned from the server. Set :ignore_server_caps, see the notes in
1165
+ # search_root_dse.
1166
+ #++
1167
+ def search_subschema_entry
1168
+ rs = search(:ignore_server_caps => true, :base => "",
1169
+ :scope => SearchScope_BaseObject,
1170
+ :attributes => [:subschemaSubentry])
1171
+ return Net::LDAP::Entry.new unless (rs and rs.first)
1172
+
1173
+ subschema_name = rs.first.subschemasubentry
1174
+ return Net::LDAP::Entry.new unless (subschema_name and subschema_name.first)
1175
+
1176
+ rs = search(:ignore_server_caps => true, :base => subschema_name.first,
1177
+ :scope => SearchScope_BaseObject,
1178
+ :filter => "objectclass=subschema",
1179
+ :attributes => [:objectclasses, :attributetypes])
1180
+ (rs and rs.first) or Net::LDAP::Entry.new
1181
+ end
1182
+
1183
+ #--
1184
+ # Convenience method to query server capabilities.
1185
+ # Only do this once per Net::LDAP object.
1186
+ # Note, we call a search, and we might be called from inside a search!
1187
+ # MUST refactor the root_dse call out.
1188
+ #++
1189
+ def paged_searches_supported?
1190
+ # active directory returns that it supports paged results. However
1191
+ # it returns binary data in the rfc2696_cookie which throws an
1192
+ # encoding exception breaking searching.
1193
+ return false if @force_no_page
1194
+ @server_caps ||= search_root_dse
1195
+ @server_caps[:supportedcontrol].include?(Net::LDAP::LDAPControls::PAGED_RESULTS)
1196
+ end
1197
+
1198
+ private
1199
+
1200
+ # Yields an open connection if there is one, otherwise establishes a new
1201
+ # connection, binds, and yields it. If binding fails, it will return the
1202
+ # result from that, and :use_connection: will not yield at all. If not
1203
+ # the return value is whatever is returned from the block.
1204
+ def use_connection(args)
1205
+ if @open_connection
1206
+ yield @open_connection
1207
+ else
1208
+ begin
1209
+ conn = new_connection
1210
+ if (result = conn.bind(args[:auth] || @auth)).result_code == Net::LDAP::ResultCodeSuccess
1211
+ yield conn
1212
+ else
1213
+ return result
1214
+ end
1215
+ ensure
1216
+ conn.close if conn
1217
+ end
1218
+ end
1219
+ end
1220
+
1221
+ # Establish a new connection to the LDAP server
1222
+ def new_connection
1223
+ Net::LDAP::Connection.new \
1224
+ :host => @host,
1225
+ :port => @port,
1226
+ :encryption => @encryption,
1227
+ :instrumentation_service => @instrumentation_service
1228
+ end
1229
+ end # class LDAP