rubinius-net-ldap 0.11

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