adams-net-ldap 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +15 -0
  2. data/.autotest +11 -0
  3. data/.rspec +2 -0
  4. data/Contributors.rdoc +22 -0
  5. data/Hacking.rdoc +68 -0
  6. data/History.rdoc +186 -0
  7. data/License.rdoc +29 -0
  8. data/Manifest.txt +49 -0
  9. data/README.rdoc +52 -0
  10. data/Rakefile +74 -0
  11. data/autotest/discover.rb +1 -0
  12. data/lib/net-ldap.rb +2 -0
  13. data/lib/net/ber.rb +318 -0
  14. data/lib/net/ber/ber_parser.rb +168 -0
  15. data/lib/net/ber/core_ext.rb +62 -0
  16. data/lib/net/ber/core_ext/array.rb +96 -0
  17. data/lib/net/ber/core_ext/bignum.rb +22 -0
  18. data/lib/net/ber/core_ext/false_class.rb +10 -0
  19. data/lib/net/ber/core_ext/fixnum.rb +66 -0
  20. data/lib/net/ber/core_ext/string.rb +76 -0
  21. data/lib/net/ber/core_ext/true_class.rb +12 -0
  22. data/lib/net/ldap.rb +1633 -0
  23. data/lib/net/ldap/dataset.rb +154 -0
  24. data/lib/net/ldap/dn.rb +225 -0
  25. data/lib/net/ldap/entry.rb +185 -0
  26. data/lib/net/ldap/filter.rb +781 -0
  27. data/lib/net/ldap/password.rb +37 -0
  28. data/lib/net/ldap/pdu.rb +273 -0
  29. data/lib/net/snmp.rb +268 -0
  30. data/net-ldap.gemspec +58 -0
  31. data/spec/integration/ssl_ber_spec.rb +36 -0
  32. data/spec/spec.opts +2 -0
  33. data/spec/spec_helper.rb +5 -0
  34. data/spec/unit/ber/ber_spec.rb +114 -0
  35. data/spec/unit/ber/core_ext/string_spec.rb +51 -0
  36. data/spec/unit/ldap/dn_spec.rb +80 -0
  37. data/spec/unit/ldap/entry_spec.rb +51 -0
  38. data/spec/unit/ldap/filter_spec.rb +84 -0
  39. data/spec/unit/ldap_spec.rb +78 -0
  40. data/test/common.rb +3 -0
  41. data/test/test_entry.rb +59 -0
  42. data/test/test_filter.rb +122 -0
  43. data/test/test_ldap_connection.rb +24 -0
  44. data/test/test_ldif.rb +79 -0
  45. data/test/test_password.rb +17 -0
  46. data/test/test_rename.rb +77 -0
  47. data/test/test_snmp.rb +114 -0
  48. data/test/testdata.ldif +101 -0
  49. data/testserver/ldapserver.rb +210 -0
  50. data/testserver/testdata.ldif +101 -0
  51. metadata +221 -0
@@ -0,0 +1,62 @@
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/bignum'
37
+ # :stopdoc:
38
+ class Bignum
39
+ include Net::BER::Extensions::Bignum
40
+ end
41
+ # :startdoc:
42
+
43
+ require 'net/ber/core_ext/fixnum'
44
+ # :stopdoc:
45
+ class Fixnum
46
+ include Net::BER::Extensions::Fixnum
47
+ end
48
+ # :startdoc:
49
+
50
+ require 'net/ber/core_ext/true_class'
51
+ # :stopdoc:
52
+ class TrueClass
53
+ include Net::BER::Extensions::TrueClass
54
+ end
55
+ # :startdoc:
56
+
57
+ require 'net/ber/core_ext/false_class'
58
+ # :stopdoc:
59
+ class FalseClass
60
+ include Net::BER::Extensions::FalseClass
61
+ end
62
+ # :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,22 @@
1
+ # -*- ruby encoding: utf-8 -*-
2
+ ##
3
+ # BER extensions to the Bignum class.
4
+ module Net::BER::Extensions::Bignum
5
+ ##
6
+ # Converts a Bignum to an uncompressed BER integer.
7
+ def to_ber
8
+ result = []
9
+
10
+ # NOTE: Array#pack's 'w' is a BER _compressed_ integer. We need
11
+ # uncompressed BER integers, so we're not using that. See also:
12
+ # http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/228864
13
+ n = self
14
+ while n > 0
15
+ b = n & 0xff
16
+ result << b
17
+ n = n >> 8
18
+ end
19
+
20
+ "\002" + ([result.size] + result.reverse).pack('C*')
21
+ end
22
+ 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,66 @@
1
+ # -*- ruby encoding: utf-8 -*-
2
+ ##
3
+ # Ber extensions to the Fixnum class.
4
+ module Net::BER::Extensions::Fixnum
5
+ ##
6
+ # Converts the fixnum to BER format.
7
+ def to_ber
8
+ "\002#{to_ber_internal}"
9
+ end
10
+
11
+ ##
12
+ # Converts the fixnum to BER enumerated format.
13
+ def to_ber_enumerated
14
+ "\012#{to_ber_internal}"
15
+ end
16
+
17
+ ##
18
+ # Converts the fixnum to BER length encodining 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 a Fixnum. Callers
37
+ # must prepend the tag byte for the contained value.
38
+ def to_ber_internal
39
+ # CAUTION: Bit twiddling ahead. You might want to shield your eyes or
40
+ # something.
41
+
42
+ # Looks for the first byte in the fixnum that is not all zeroes. It does
43
+ # this by masking one byte after another, checking the result for bits
44
+ # that are left on.
45
+ size = Net::BER::MAX_FIXNUM_SIZE
46
+ while size > 1
47
+ break if (self & (0xff << (size - 1) * 8)) > 0
48
+ size -= 1
49
+ end
50
+
51
+ # Store the size of the fixnum in the result
52
+ result = [size]
53
+
54
+ # Appends bytes to result, starting with higher orders first. Extraction
55
+ # of bytes is done by right shifting the original fixnum by an amount
56
+ # and then masking that with 0xff.
57
+ while size > 0
58
+ # right shift size - 1 bytes, mask with 0xff
59
+ result << ((self >> ((size - 1) * 8)) & 0xff)
60
+ size -= 1
61
+ end
62
+
63
+ result.pack('C*')
64
+ end
65
+ private :to_ber_internal
66
+ end
@@ -0,0 +1,76 @@
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
+ if self.respond_to?(:encode)
29
+ # Strings should be UTF-8 encoded according to LDAP.
30
+ # However, the BER code is not necessarily valid UTF-8
31
+ begin
32
+ self.encode('UTF-8').force_encoding('ASCII-8BIT')
33
+ rescue Encoding::UndefinedConversionError
34
+ self
35
+ end
36
+ else
37
+ self
38
+ end
39
+ end
40
+ private :raw_utf8_encoded
41
+
42
+ ##
43
+ # Creates an application-specific BER string encoded value with the
44
+ # provided syntax code value.
45
+ def to_ber_application_string(code)
46
+ to_ber(0x40 + code)
47
+ end
48
+
49
+ ##
50
+ # Creates a context-specific BER string encoded value with the provided
51
+ # syntax code value.
52
+ def to_ber_contextspecific(code)
53
+ to_ber(0x80 + code)
54
+ end
55
+
56
+ ##
57
+ # Nondestructively reads a BER object from this string.
58
+ def read_ber(syntax = nil)
59
+ StringIO.new(self).read_ber(syntax)
60
+ end
61
+
62
+ ##
63
+ # Destructively reads a BER object from the string.
64
+ def read_ber!(syntax = nil)
65
+ io = StringIO.new(self)
66
+
67
+ result = io.read_ber(syntax)
68
+ self.slice!(0...io.pos)
69
+
70
+ return result
71
+ end
72
+
73
+ def reject_empty_ber_arrays
74
+ self.gsub(/0\000/n,'')
75
+ end
76
+ end
@@ -0,0 +1,12 @@
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
+ # 20100319 AZ: Note that this may not be the completely correct value,
9
+ # per some test documentation. We need to determine the truth of this.
10
+ "\001\001\001"
11
+ end
12
+ end
@@ -0,0 +1,1633 @@
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
+
27
+ # == Quick-start for the Impatient
28
+ # === Quick Example of a user-authentication against an LDAP directory:
29
+ #
30
+ # require 'rubygems'
31
+ # require 'net/ldap'
32
+ #
33
+ # ldap = Net::LDAP.new
34
+ # ldap.host = your_server_ip_address
35
+ # ldap.port = 389
36
+ # ldap.auth "joe_user", "opensesame"
37
+ # if ldap.bind
38
+ # # authentication succeeded
39
+ # else
40
+ # # authentication failed
41
+ # end
42
+ #
43
+ #
44
+ # === Quick Example of a search against an LDAP directory:
45
+ #
46
+ # require 'rubygems'
47
+ # require 'net/ldap'
48
+ #
49
+ # ldap = Net::LDAP.new :host => server_ip_address,
50
+ # :port => 389,
51
+ # :auth => {
52
+ # :method => :simple,
53
+ # :username => "cn=manager, dc=example, dc=com",
54
+ # :password => "opensesame"
55
+ # }
56
+ #
57
+ # filter = Net::LDAP::Filter.eq("cn", "George*")
58
+ # treebase = "dc=example, dc=com"
59
+ #
60
+ # ldap.search(:base => treebase, :filter => filter) do |entry|
61
+ # puts "DN: #{entry.dn}"
62
+ # entry.each do |attribute, values|
63
+ # puts " #{attribute}:"
64
+ # values.each do |value|
65
+ # puts " --->#{value}"
66
+ # end
67
+ # end
68
+ # end
69
+ #
70
+ # p ldap.get_operation_result
71
+ #
72
+ #
73
+ # == A Brief Introduction to LDAP
74
+ #
75
+ # We're going to provide a quick, informal introduction to LDAP terminology
76
+ # and typical operations. If you're comfortable with this material, skip
77
+ # ahead to "How to use Net::LDAP." If you want a more rigorous treatment of
78
+ # this material, we recommend you start with the various IETF and ITU
79
+ # standards that relate to LDAP.
80
+ #
81
+ # === Entities
82
+ # LDAP is an Internet-standard protocol used to access directory servers.
83
+ # The basic search unit is the <i>entity, </i> which corresponds to a person
84
+ # or other domain-specific object. A directory service which supports the
85
+ # LDAP protocol typically stores information about a number of entities.
86
+ #
87
+ # === Principals
88
+ # LDAP servers are typically used to access information about people, but
89
+ # also very often about such items as printers, computers, and other
90
+ # resources. To reflect this, LDAP uses the term <i>entity, </i> or less
91
+ # commonly, <i>principal, </i> to denote its basic data-storage unit.
92
+ #
93
+ # === Distinguished Names
94
+ # In LDAP's view of the world, an entity is uniquely identified by a
95
+ # globally-unique text string called a <i>Distinguished Name, </i> originally
96
+ # defined in the X.400 standards from which LDAP is ultimately derived. Much
97
+ # like a DNS hostname, a DN is a "flattened" text representation of a string
98
+ # of tree nodes. Also like DNS (and unlike Java package names), a DN
99
+ # expresses a chain of tree-nodes written from left to right in order from
100
+ # the most-resolved node to the most-general one.
101
+ #
102
+ # If you know the DN of a person or other entity, then you can query an
103
+ # LDAP-enabled directory for information (attributes) about the entity.
104
+ # Alternatively, you can query the directory for a list of DNs matching a
105
+ # set of criteria that you supply.
106
+ #
107
+ # === Attributes
108
+ #
109
+ # In the LDAP view of the world, a DN uniquely identifies an entity.
110
+ # Information about the entity is stored as a set of <i>Attributes.</i> An
111
+ # attribute is a text string which is associated with zero or more values.
112
+ # Most LDAP-enabled directories store a well-standardized range of
113
+ # attributes, and constrain their values according to standard rules.
114
+ #
115
+ # A good example of an attribute is <tt>sn, </tt> which stands for "Surname."
116
+ # This attribute is generally used to store a person's surname, or last
117
+ # name. Most directories enforce the standard convention that an entity's
118
+ # <tt>sn</tt> attribute have <i>exactly one</i> value. In LDAP jargon, that
119
+ # means that <tt>sn</tt> must be <i>present</i> and <i>single-valued.</i>
120
+ #
121
+ # Another attribute is <tt>mail, </tt> which is used to store email
122
+ # addresses. (No, there is no attribute called "email, " perhaps because
123
+ # X.400 terminology predates the invention of the term <i>email.</i>)
124
+ # <tt>mail</tt> differs from <tt>sn</tt> in that most directories permit any
125
+ # number of values for the <tt>mail</tt> attribute, including zero.
126
+ #
127
+ # === Tree-Base
128
+ # We said above that X.400 Distinguished Names are <i>globally unique.</i>
129
+ # In a manner reminiscent of DNS, LDAP supposes that each directory server
130
+ # contains authoritative attribute data for a set of DNs corresponding to a
131
+ # specific sub-tree of the (notional) global directory tree. This subtree is
132
+ # generally configured into a directory server when it is created. It
133
+ # matters for this discussion because most servers will not allow you to
134
+ # query them unless you specify a correct tree-base.
135
+ #
136
+ # Let's say you work for the engineering department of Big Company, Inc.,
137
+ # whose internet domain is bigcompany.com. You may find that your
138
+ # departmental directory is stored in a server with a defined tree-base of
139
+ # ou=engineering, dc=bigcompany, dc=com
140
+ # You will need to supply this string as the <i>tree-base</i> when querying
141
+ # this directory. (Ou is a very old X.400 term meaning "organizational
142
+ # unit." Dc is a more recent term meaning "domain component.")
143
+ #
144
+ # === LDAP Versions
145
+ # (stub, discuss v2 and v3)
146
+ #
147
+ # === LDAP Operations
148
+ # The essential operations are: #bind, #search, #add, #modify, #delete, and
149
+ # #rename.
150
+ #
151
+ # ==== Bind
152
+ # #bind supplies a user's authentication credentials to a server, which in
153
+ # turn verifies or rejects them. There is a range of possibilities for
154
+ # credentials, but most directories support a simple username and password
155
+ # authentication.
156
+ #
157
+ # Taken by itself, #bind can be used to authenticate a user against
158
+ # information stored in a directory, for example to permit or deny access to
159
+ # some other resource. In terms of the other LDAP operations, most
160
+ # directories require a successful #bind to be performed before the other
161
+ # operations will be permitted. Some servers permit certain operations to be
162
+ # performed with an "anonymous" binding, meaning that no credentials are
163
+ # presented by the user. (We're glossing over a lot of platform-specific
164
+ # detail here.)
165
+ #
166
+ # ==== Search
167
+ # Calling #search against the directory involves specifying a treebase, a
168
+ # set of <i>search filters, </i> and a list of attribute values. The filters
169
+ # specify ranges of possible values for particular attributes. Multiple
170
+ # filters can be joined together with AND, OR, and NOT operators. A server
171
+ # will respond to a #search by returning a list of matching DNs together
172
+ # with a set of attribute values for each entity, depending on what
173
+ # attributes the search requested.
174
+ #
175
+ # ==== Add
176
+ # #add specifies a new DN and an initial set of attribute values. If the
177
+ # operation succeeds, a new entity with the corresponding DN and attributes
178
+ # is added to the directory.
179
+ #
180
+ # ==== Modify
181
+ # #modify specifies an entity DN, and a list of attribute operations.
182
+ # #modify is used to change the attribute values stored in the directory for
183
+ # a particular entity. #modify may add or delete attributes (which are lists
184
+ # of values) or it change attributes by adding to or deleting from their
185
+ # values. Net::LDAP provides three easier methods to modify an entry's
186
+ # attribute values: #add_attribute, #replace_attribute, and
187
+ # #delete_attribute.
188
+ #
189
+ # ==== Delete
190
+ # #delete specifies an entity DN. If it succeeds, the entity and all its
191
+ # attributes is removed from the directory.
192
+ #
193
+ # ==== Rename (or Modify RDN)
194
+ # #rename (or #modify_rdn) is an operation added to version 3 of the LDAP
195
+ # protocol. It responds to the often-arising need to change the DN of an
196
+ # entity without discarding its attribute values. In earlier LDAP versions,
197
+ # the only way to do this was to delete the whole entity and add it again
198
+ # with a different DN.
199
+ #
200
+ # #rename works by taking an "old" DN (the one to change) and a "new RDN, "
201
+ # which is the left-most part of the DN string. If successful, #rename
202
+ # changes the entity DN so that its left-most node corresponds to the new
203
+ # RDN given in the request. (RDN, or "relative distinguished name, " denotes
204
+ # a single tree-node as expressed in a DN, which is a chain of tree nodes.)
205
+ #
206
+ # == How to use Net::LDAP
207
+ # To access Net::LDAP functionality in your Ruby programs, start by
208
+ # requiring the library:
209
+ #
210
+ # require 'net/ldap'
211
+ #
212
+ # If you installed the Gem version of Net::LDAP, and depending on your
213
+ # version of Ruby and rubygems, you _may_ also need to require rubygems
214
+ # explicitly:
215
+ #
216
+ # require 'rubygems'
217
+ # require 'net/ldap'
218
+ #
219
+ # Most operations with Net::LDAP start by instantiating a Net::LDAP object.
220
+ # The constructor for this object takes arguments specifying the network
221
+ # location (address and port) of the LDAP server, and also the binding
222
+ # (authentication) credentials, typically a username and password. Given an
223
+ # object of class Net:LDAP, you can then perform LDAP operations by calling
224
+ # instance methods on the object. These are documented with usage examples
225
+ # below.
226
+ #
227
+ # The Net::LDAP library is designed to be very disciplined about how it
228
+ # makes network connections to servers. This is different from many of the
229
+ # standard native-code libraries that are provided on most platforms, which
230
+ # share bloodlines with the original Netscape/Michigan LDAP client
231
+ # implementations. These libraries sought to insulate user code from the
232
+ # workings of the network. This is a good idea of course, but the practical
233
+ # effect has been confusing and many difficult bugs have been caused by the
234
+ # opacity of the native libraries, and their variable behavior across
235
+ # platforms.
236
+ #
237
+ # In general, Net::LDAP instance methods which invoke server operations make
238
+ # a connection to the server when the method is called. They execute the
239
+ # operation (typically binding first) and then disconnect from the server.
240
+ # The exception is Net::LDAP#open, which makes a connection to the server
241
+ # and then keeps it open while it executes a user-supplied block.
242
+ # Net::LDAP#open closes the connection on completion of the block.
243
+ class Net::LDAP
244
+ VERSION = "0.4.0"
245
+
246
+ class LdapError < StandardError; end
247
+
248
+ SearchScope_BaseObject = 0
249
+ SearchScope_SingleLevel = 1
250
+ SearchScope_WholeSubtree = 2
251
+ SearchScopes = [ SearchScope_BaseObject, SearchScope_SingleLevel,
252
+ SearchScope_WholeSubtree ]
253
+
254
+ primitive = { 2 => :null } # UnbindRequest body
255
+ constructed = {
256
+ 0 => :array, # BindRequest
257
+ 1 => :array, # BindResponse
258
+ 2 => :array, # UnbindRequest
259
+ 3 => :array, # SearchRequest
260
+ 4 => :array, # SearchData
261
+ 5 => :array, # SearchResult
262
+ 6 => :array, # ModifyRequest
263
+ 7 => :array, # ModifyResponse
264
+ 8 => :array, # AddRequest
265
+ 9 => :array, # AddResponse
266
+ 10 => :array, # DelRequest
267
+ 11 => :array, # DelResponse
268
+ 12 => :array, # ModifyRdnRequest
269
+ 13 => :array, # ModifyRdnResponse
270
+ 14 => :array, # CompareRequest
271
+ 15 => :array, # CompareResponse
272
+ 16 => :array, # AbandonRequest
273
+ 19 => :array, # SearchResultReferral
274
+ 24 => :array, # Unsolicited Notification
275
+ }
276
+ application = {
277
+ :primitive => primitive,
278
+ :constructed => constructed,
279
+ }
280
+ primitive = {
281
+ 0 => :string, # password
282
+ 1 => :string, # Kerberos v4
283
+ 2 => :string, # Kerberos v5
284
+ 3 => :string, # SearchFilter-extensible
285
+ 4 => :string, # SearchFilter-extensible
286
+ 7 => :string, # serverSaslCreds
287
+ }
288
+ constructed = {
289
+ 0 => :array, # RFC-2251 Control and Filter-AND
290
+ 1 => :array, # SearchFilter-OR
291
+ 2 => :array, # SearchFilter-NOT
292
+ 3 => :array, # Seach referral
293
+ 4 => :array, # unknown use in Microsoft Outlook
294
+ 5 => :array, # SearchFilter-GE
295
+ 6 => :array, # SearchFilter-LE
296
+ 7 => :array, # serverSaslCreds
297
+ 9 => :array, # SearchFilter-extensible
298
+ }
299
+ context_specific = {
300
+ :primitive => primitive,
301
+ :constructed => constructed,
302
+ }
303
+
304
+ AsnSyntax = Net::BER.compile_syntax(:application => application,
305
+ :context_specific => context_specific)
306
+
307
+ DefaultHost = "127.0.0.1"
308
+ DefaultPort = 389
309
+ DefaultAuth = { :method => :anonymous }
310
+ DefaultTreebase = "dc=com"
311
+ DefaultForceNoPage = false
312
+
313
+ StartTlsOid = "1.3.6.1.4.1.1466.20037"
314
+
315
+ ResultStrings = {
316
+ 0 => "Success",
317
+ 1 => "Operations Error",
318
+ 2 => "Protocol Error",
319
+ 3 => "Time Limit Exceeded",
320
+ 4 => "Size Limit Exceeded",
321
+ 10 => "Referral",
322
+ 12 => "Unavailable crtical extension",
323
+ 14 => "saslBindInProgress",
324
+ 16 => "No Such Attribute",
325
+ 17 => "Undefined Attribute Type",
326
+ 19 => "Constraint Violation",
327
+ 20 => "Attribute or Value Exists",
328
+ 32 => "No Such Object",
329
+ 34 => "Invalid DN Syntax",
330
+ 48 => "Inappropriate Authentication",
331
+ 49 => "Invalid Credentials",
332
+ 50 => "Insufficient Access Rights",
333
+ 51 => "Busy",
334
+ 52 => "Unavailable",
335
+ 53 => "Unwilling to perform",
336
+ 65 => "Object Class Violation",
337
+ 68 => "Entry Already Exists"
338
+ }
339
+
340
+ module LDAPControls
341
+ PAGED_RESULTS = "1.2.840.113556.1.4.319" # Microsoft evil from RFC 2696
342
+ SORT_REQUEST = "1.2.840.113556.1.4.473"
343
+ SORT_RESPONSE = "1.2.840.113556.1.4.474"
344
+ DELETE_TREE = "1.2.840.113556.1.4.805"
345
+ end
346
+
347
+ def self.result2string(code) #:nodoc:
348
+ ResultStrings[code] || "unknown result (#{code})"
349
+ end
350
+
351
+ attr_accessor :host
352
+ attr_accessor :port
353
+ attr_accessor :base
354
+
355
+ # Instantiate an object of type Net::LDAP to perform directory operations.
356
+ # This constructor takes a Hash containing arguments, all of which are
357
+ # either optional or may be specified later with other methods as
358
+ # described below. The following arguments are supported:
359
+ # * :host => the LDAP server's IP-address (default 127.0.0.1)
360
+ # * :port => the LDAP server's TCP port (default 389)
361
+ # * :auth => a Hash containing authorization parameters. Currently
362
+ # supported values include: {:method => :anonymous} and {:method =>
363
+ # :simple, :username => your_user_name, :password => your_password }
364
+ # The password parameter may be a Proc that returns a String.
365
+ # * :base => a default treebase parameter for searches performed against
366
+ # the LDAP server. If you don't give this value, then each call to
367
+ # #search must specify a treebase parameter. If you do give this value,
368
+ # then it will be used in subsequent calls to #search that do not
369
+ # specify a treebase. If you give a treebase value in any particular
370
+ # call to #search, that value will override any treebase value you give
371
+ # here.
372
+ # * :encryption => specifies the encryption to be used in communicating
373
+ # with the LDAP server. The value is either a Hash containing additional
374
+ # parameters, or the Symbol :simple_tls, which is equivalent to
375
+ # specifying the Hash {:method => :simple_tls}. There is a fairly large
376
+ # range of potential values that may be given for this parameter. See
377
+ # #encryption for details.
378
+ # * :force_no_page => Set to true to prevent paged results even if your
379
+ # server says it supports them. This is a fix for MS Active Directory
380
+ #
381
+ # Instantiating a Net::LDAP object does <i>not</i> result in network
382
+ # traffic to the LDAP server. It simply stores the connection and binding
383
+ # parameters in the object.
384
+ def initialize(args = {})
385
+ @host = args[:host] || DefaultHost
386
+ @port = args[:port] || DefaultPort
387
+ @verbose = false # Make this configurable with a switch on the class.
388
+ @auth = args[:auth] || DefaultAuth
389
+ @base = args[:base] || DefaultTreebase
390
+ @force_no_page = args[:force_no_page] || DefaultForceNoPage
391
+ encryption args[:encryption] # may be nil
392
+
393
+ if pr = @auth[:password] and pr.respond_to?(:call)
394
+ @auth[:password] = pr.call
395
+ end
396
+
397
+ # This variable is only set when we are created with LDAP::open. All of
398
+ # our internal methods will connect using it, or else they will create
399
+ # their own.
400
+ @open_connection = nil
401
+ end
402
+
403
+ # Convenience method to specify authentication credentials to the LDAP
404
+ # server. Currently supports simple authentication requiring a username
405
+ # and password.
406
+ #
407
+ # Observe that on most LDAP servers, the username is a complete DN.
408
+ # However, with A/D, it's often possible to give only a user-name rather
409
+ # than a complete DN. In the latter case, beware that many A/D servers are
410
+ # configured to permit anonymous (uncredentialled) binding, and will
411
+ # silently accept your binding as anonymous if you give an unrecognized
412
+ # username. This is not usually what you want. (See
413
+ # #get_operation_result.)
414
+ #
415
+ # <b>Important:</b> The password argument may be a Proc that returns a
416
+ # string. This makes it possible for you to write client programs that
417
+ # solicit passwords from users or from other data sources without showing
418
+ # them in your code or on command lines.
419
+ #
420
+ # require 'net/ldap'
421
+ #
422
+ # ldap = Net::LDAP.new
423
+ # ldap.host = server_ip_address
424
+ # ldap.authenticate "cn=Your Username, cn=Users, dc=example, dc=com", "your_psw"
425
+ #
426
+ # Alternatively (with a password block):
427
+ #
428
+ # require 'net/ldap'
429
+ #
430
+ # ldap = Net::LDAP.new
431
+ # ldap.host = server_ip_address
432
+ # psw = proc { your_psw_function }
433
+ # ldap.authenticate "cn=Your Username, cn=Users, dc=example, dc=com", psw
434
+ #
435
+ def authenticate(username, password)
436
+ password = password.call if password.respond_to?(:call)
437
+ @auth = {
438
+ :method => :simple,
439
+ :username => username,
440
+ :password => password
441
+ }
442
+ end
443
+ alias_method :auth, :authenticate
444
+
445
+ # Convenience method to specify encryption characteristics for connections
446
+ # to LDAP servers. Called implicitly by #new and #open, but may also be
447
+ # called by user code if desired. The single argument is generally a Hash
448
+ # (but see below for convenience alternatives). This implementation is
449
+ # currently a stub, supporting only a few encryption alternatives. As
450
+ # additional capabilities are added, more configuration values will be
451
+ # added here.
452
+ #
453
+ # Currently, the only supported argument is { :method => :simple_tls }.
454
+ # (Equivalently, you may pass the symbol :simple_tls all by itself,
455
+ # without enclosing it in a Hash.)
456
+ #
457
+ # The :simple_tls encryption method encrypts <i>all</i> communications
458
+ # with the LDAP server. It completely establishes SSL/TLS encryption with
459
+ # the LDAP server before any LDAP-protocol data is exchanged. There is no
460
+ # plaintext negotiation and no special encryption-request controls are
461
+ # sent to the server. <i>The :simple_tls option is the simplest, easiest
462
+ # way to encrypt communications between Net::LDAP and LDAP servers.</i>
463
+ # It's intended for cases where you have an implicit level of trust in the
464
+ # authenticity of the LDAP server. No validation of the LDAP server's SSL
465
+ # certificate is performed. This means that :simple_tls will not produce
466
+ # errors if the LDAP server's encryption certificate is not signed by a
467
+ # well-known Certification Authority. If you get communications or
468
+ # protocol errors when using this option, check with your LDAP server
469
+ # administrator. Pay particular attention to the TCP port you are
470
+ # connecting to. It's impossible for an LDAP server to support plaintext
471
+ # LDAP communications and <i>simple TLS</i> connections on the same port.
472
+ # The standard TCP port for unencrypted LDAP connections is 389, but the
473
+ # standard port for simple-TLS encrypted connections is 636. Be sure you
474
+ # are using the correct port.
475
+ #
476
+ # <i>[Note: a future version of Net::LDAP will support the STARTTLS LDAP
477
+ # control, which will enable encrypted communications on the same TCP port
478
+ # used for unencrypted connections.]</i>
479
+ def encryption(args)
480
+ case args
481
+ when :simple_tls, :start_tls
482
+ args = { :method => args }
483
+ end
484
+ @encryption = args
485
+ end
486
+
487
+ # #open takes the same parameters as #new. #open makes a network
488
+ # connection to the LDAP server and then passes a newly-created Net::LDAP
489
+ # object to the caller-supplied block. Within the block, you can call any
490
+ # of the instance methods of Net::LDAP to perform operations against the
491
+ # LDAP directory. #open will perform all the operations in the
492
+ # user-supplied block on the same network connection, which will be closed
493
+ # automatically when the block finishes.
494
+ #
495
+ # # (PSEUDOCODE)
496
+ # auth = { :method => :simple, :username => username, :password => password }
497
+ # Net::LDAP.open(:host => ipaddress, :port => 389, :auth => auth) do |ldap|
498
+ # ldap.search(...)
499
+ # ldap.add(...)
500
+ # ldap.modify(...)
501
+ # end
502
+ def self.open(args)
503
+ ldap1 = new(args)
504
+ ldap1.open { |ldap| yield ldap }
505
+ end
506
+
507
+ # Returns a meaningful result any time after a protocol operation (#bind,
508
+ # #search, #add, #modify, #rename, #delete) has completed. It returns an
509
+ # #OpenStruct containing an LDAP result code (0 means success), and a
510
+ # human-readable string.
511
+ #
512
+ # unless ldap.bind
513
+ # puts "Result: #{ldap.get_operation_result.code}"
514
+ # puts "Message: #{ldap.get_operation_result.message}"
515
+ # end
516
+ #
517
+ # Certain operations return additional information, accessible through
518
+ # members of the object returned from #get_operation_result. Check
519
+ # #get_operation_result.error_message and
520
+ # #get_operation_result.matched_dn.
521
+ #
522
+ #--
523
+ # Modified the implementation, 20Mar07. We might get a hash of LDAP
524
+ # response codes instead of a simple numeric code.
525
+ #++
526
+ def get_operation_result
527
+ result = @result
528
+ result = result.result if result.is_a?(Net::LDAP::PDU)
529
+ os = OpenStruct.new
530
+ if result.is_a?(Hash)
531
+ # We might get a hash of LDAP response codes instead of a simple
532
+ # numeric code.
533
+ os.code = (result[:resultCode] || "").to_i
534
+ os.error_message = result[:errorMessage]
535
+ os.matched_dn = result[:matchedDN]
536
+ elsif result
537
+ os.code = result
538
+ else
539
+ os.code = 0
540
+ end
541
+ os.message = Net::LDAP.result2string(os.code)
542
+ os
543
+ end
544
+
545
+ # Opens a network connection to the server and then passes <tt>self</tt>
546
+ # to the caller-supplied block. The connection is closed when the block
547
+ # completes. Used for executing multiple LDAP operations without requiring
548
+ # a separate network connection (and authentication) for each one.
549
+ # <i>Note:</i> You do not need to log-in or "bind" to the server. This
550
+ # will be done for you automatically. For an even simpler approach, see
551
+ # the class method Net::LDAP#open.
552
+ #
553
+ # # (PSEUDOCODE)
554
+ # auth = { :method => :simple, :username => username, :password => password }
555
+ # ldap = Net::LDAP.new(:host => ipaddress, :port => 389, :auth => auth)
556
+ # ldap.open do |ldap|
557
+ # ldap.search(...)
558
+ # ldap.add(...)
559
+ # ldap.modify(...)
560
+ # end
561
+ def open
562
+ # First we make a connection and then a binding, but we don't do
563
+ # anything with the bind results. We then pass self to the caller's
564
+ # block, where he will execute his LDAP operations. Of course they will
565
+ # all generate auth failures if the bind was unsuccessful.
566
+ raise Net::LDAP::LdapError, "Open already in progress" if @open_connection
567
+
568
+ begin
569
+ @open_connection = Net::LDAP::Connection.new(:host => @host,
570
+ :port => @port,
571
+ :encryption =>
572
+ @encryption)
573
+ @open_connection.bind(@auth)
574
+ yield self
575
+ ensure
576
+ @open_connection.close if @open_connection
577
+ @open_connection = nil
578
+ end
579
+ end
580
+
581
+ # Searches the LDAP directory for directory entries. Takes a hash argument
582
+ # with parameters. Supported parameters include:
583
+ # * :base (a string specifying the tree-base for the search);
584
+ # * :filter (an object of type Net::LDAP::Filter, defaults to
585
+ # objectclass=*);
586
+ # * :attributes (a string or array of strings specifying the LDAP
587
+ # attributes to return from the server);
588
+ # * :return_result (a boolean specifying whether to return a result set).
589
+ # * :attributes_only (a boolean flag, defaults false)
590
+ # * :scope (one of: Net::LDAP::SearchScope_BaseObject,
591
+ # Net::LDAP::SearchScope_SingleLevel,
592
+ # Net::LDAP::SearchScope_WholeSubtree. Default is WholeSubtree.)
593
+ # * :size (an integer indicating the maximum number of search entries to
594
+ # return. Default is zero, which signifies no limit.)
595
+ #
596
+ # #search queries the LDAP server and passes <i>each entry</i> to the
597
+ # caller-supplied block, as an object of type Net::LDAP::Entry. If the
598
+ # search returns 1000 entries, the block will be called 1000 times. If the
599
+ # search returns no entries, the block will not be called.
600
+ #
601
+ # #search returns either a result-set or a boolean, depending on the value
602
+ # of the <tt>:return_result</tt> argument. The default behavior is to
603
+ # return a result set, which is an Array of objects of class
604
+ # Net::LDAP::Entry. If you request a result set and #search fails with an
605
+ # error, it will return nil. Call #get_operation_result to get the error
606
+ # information returned by
607
+ # the LDAP server.
608
+ #
609
+ # When <tt>:return_result => false, </tt> #search will return only a
610
+ # Boolean, to indicate whether the operation succeeded. This can improve
611
+ # performance with very large result sets, because the library can discard
612
+ # each entry from memory after your block processes it.
613
+ #
614
+ # treebase = "dc=example, dc=com"
615
+ # filter = Net::LDAP::Filter.eq("mail", "a*.com")
616
+ # attrs = ["mail", "cn", "sn", "objectclass"]
617
+ # ldap.search(:base => treebase, :filter => filter, :attributes => attrs,
618
+ # :return_result => false) do |entry|
619
+ # puts "DN: #{entry.dn}"
620
+ # entry.each do |attr, values|
621
+ # puts ".......#{attr}:"
622
+ # values.each do |value|
623
+ # puts " #{value}"
624
+ # end
625
+ # end
626
+ # end
627
+ def search(args = {})
628
+ unless args[:ignore_server_caps]
629
+ args[:paged_searches_supported] = paged_searches_supported?
630
+ end
631
+
632
+ args[:base] ||= @base
633
+ return_result_set = args[:return_result] != false
634
+ result_set = return_result_set ? [] : nil
635
+
636
+ if @open_connection
637
+ @result = @open_connection.search(args) { |entry|
638
+ result_set << entry if result_set
639
+ yield entry if block_given?
640
+ }
641
+ else
642
+ begin
643
+ conn = Net::LDAP::Connection.new(:host => @host, :port => @port,
644
+ :encryption => @encryption)
645
+ if (@result = conn.bind(args[:auth] || @auth)).result_code == 0
646
+ @result = conn.search(args) { |entry|
647
+ result_set << entry if result_set
648
+ yield entry if block_given?
649
+ }
650
+ end
651
+ ensure
652
+ conn.close if conn
653
+ end
654
+ end
655
+
656
+ if return_result_set
657
+ (!@result.nil? && @result.result_code == 0) ? result_set : nil
658
+ else
659
+ @result.success?
660
+ end
661
+ end
662
+
663
+ # #bind connects to an LDAP server and requests authentication based on
664
+ # the <tt>:auth</tt> parameter passed to #open or #new. It takes no
665
+ # parameters.
666
+ #
667
+ # User code does not need to call #bind directly. It will be called
668
+ # implicitly by the library whenever you invoke an LDAP operation, such as
669
+ # #search or #add.
670
+ #
671
+ # It is useful, however, to call #bind in your own code when the only
672
+ # operation you intend to perform against the directory is to validate a
673
+ # login credential. #bind returns true or false to indicate whether the
674
+ # binding was successful. Reasons for failure include malformed or
675
+ # unrecognized usernames and incorrect passwords. Use
676
+ # #get_operation_result to find out what happened in case of failure.
677
+ #
678
+ # Here's a typical example using #bind to authenticate a credential which
679
+ # was (perhaps) solicited from the user of a web site:
680
+ #
681
+ # require 'net/ldap'
682
+ # ldap = Net::LDAP.new
683
+ # ldap.host = your_server_ip_address
684
+ # ldap.port = 389
685
+ # ldap.auth your_user_name, your_user_password
686
+ # if ldap.bind
687
+ # # authentication succeeded
688
+ # else
689
+ # # authentication failed
690
+ # p ldap.get_operation_result
691
+ # end
692
+ #
693
+ # Here's a more succinct example which does exactly the same thing, but
694
+ # collects all the required parameters into arguments:
695
+ #
696
+ # require 'net/ldap'
697
+ # ldap = Net::LDAP.new(:host => your_server_ip_address, :port => 389)
698
+ # if ldap.bind(:method => :simple, :username => your_user_name,
699
+ # :password => your_user_password)
700
+ # # authentication succeeded
701
+ # else
702
+ # # authentication failed
703
+ # p ldap.get_operation_result
704
+ # end
705
+ #
706
+ # You don't need to pass a user-password as a String object to bind. You
707
+ # can also pass a Ruby Proc object which returns a string. This will cause
708
+ # bind to execute the Proc (which might then solicit input from a user
709
+ # with console display suppressed). The String value returned from the
710
+ # Proc is used as the password.
711
+ #
712
+ # You don't have to create a new instance of Net::LDAP every time you
713
+ # perform a binding in this way. If you prefer, you can cache the
714
+ # Net::LDAP object and re-use it to perform subsequent bindings,
715
+ # <i>provided</i> you call #auth to specify a new credential before
716
+ # calling #bind. Otherwise, you'll just re-authenticate the previous user!
717
+ # (You don't need to re-set the values of #host and #port.) As noted in
718
+ # the documentation for #auth, the password parameter can be a Ruby Proc
719
+ # instead of a String.
720
+ def bind(auth = @auth)
721
+ if @open_connection
722
+ @result = @open_connection.bind(auth)
723
+ else
724
+ begin
725
+ conn = Connection.new(:host => @host, :port => @port,
726
+ :encryption => @encryption)
727
+ @result = conn.bind(auth)
728
+ ensure
729
+ conn.close if conn
730
+ end
731
+ end
732
+
733
+ @result.success?
734
+ end
735
+
736
+ # #bind_as is for testing authentication credentials.
737
+ #
738
+ # As described under #bind, most LDAP servers require that you supply a
739
+ # complete DN as a binding-credential, along with an authenticator such as
740
+ # a password. But for many applications (such as authenticating users to a
741
+ # Rails application), you often don't have a full DN to identify the user.
742
+ # You usually get a simple identifier like a username or an email address,
743
+ # along with a password. #bind_as allows you to authenticate these
744
+ # user-identifiers.
745
+ #
746
+ # #bind_as is a combination of a search and an LDAP binding. First, it
747
+ # connects and binds to the directory as normal. Then it searches the
748
+ # directory for an entry corresponding to the email address, username, or
749
+ # other string that you supply. If the entry exists, then #bind_as will
750
+ # <b>re-bind</b> as that user with the password (or other authenticator)
751
+ # that you supply.
752
+ #
753
+ # #bind_as takes the same parameters as #search, <i>with the addition of
754
+ # an authenticator.</i> Currently, this authenticator must be
755
+ # <tt>:password</tt>. Its value may be either a String, or a +proc+ that
756
+ # returns a String. #bind_as returns +false+ on failure. On success, it
757
+ # returns a result set, just as #search does. This result set is an Array
758
+ # of objects of type Net::LDAP::Entry. It contains the directory
759
+ # attributes corresponding to the user. (Just test whether the return
760
+ # value is logically true, if you don't need this additional information.)
761
+ #
762
+ # Here's how you would use #bind_as to authenticate an email address and
763
+ # password:
764
+ #
765
+ # require 'net/ldap'
766
+ #
767
+ # user, psw = "joe_user@yourcompany.com", "joes_psw"
768
+ #
769
+ # ldap = Net::LDAP.new
770
+ # ldap.host = "192.168.0.100"
771
+ # ldap.port = 389
772
+ # ldap.auth "cn=manager, dc=yourcompany, dc=com", "topsecret"
773
+ #
774
+ # result = ldap.bind_as(:base => "dc=yourcompany, dc=com",
775
+ # :filter => "(mail=#{user})",
776
+ # :password => psw)
777
+ # if result
778
+ # puts "Authenticated #{result.first.dn}"
779
+ # else
780
+ # puts "Authentication FAILED."
781
+ # end
782
+ def bind_as(args = {})
783
+ result = false
784
+ open { |me|
785
+ rs = search args
786
+ if rs and rs.first and dn = rs.first.dn
787
+ password = args[:password]
788
+ password = password.call if password.respond_to?(:call)
789
+ result = rs if bind(:method => :simple, :username => dn,
790
+ :password => password)
791
+ end
792
+ }
793
+ result
794
+ end
795
+
796
+ # Adds a new entry to the remote LDAP server.
797
+ # Supported arguments:
798
+ # :dn :: Full DN of the new entry
799
+ # :attributes :: Attributes of the new entry.
800
+ #
801
+ # The attributes argument is supplied as a Hash keyed by Strings or
802
+ # Symbols giving the attribute name, and mapping to Strings or Arrays of
803
+ # Strings giving the actual attribute values. Observe that most LDAP
804
+ # directories enforce schema constraints on the attributes contained in
805
+ # entries. #add will fail with a server-generated error if your attributes
806
+ # violate the server-specific constraints.
807
+ #
808
+ # Here's an example:
809
+ #
810
+ # dn = "cn=George Smith, ou=people, dc=example, dc=com"
811
+ # attr = {
812
+ # :cn => "George Smith",
813
+ # :objectclass => ["top", "inetorgperson"],
814
+ # :sn => "Smith",
815
+ # :mail => "gsmith@example.com"
816
+ # }
817
+ # Net::LDAP.open(:host => host) do |ldap|
818
+ # ldap.add(:dn => dn, :attributes => attr)
819
+ # end
820
+ def add(args)
821
+ if @open_connection
822
+ @result = @open_connection.add(args)
823
+ else
824
+ @result = 0
825
+ begin
826
+ conn = Connection.new(:host => @host, :port => @port,
827
+ :encryption => @encryption)
828
+ if (@result = conn.bind(args[:auth] || @auth)).result_code == 0
829
+ @result = conn.add(args)
830
+ end
831
+ ensure
832
+ conn.close if conn
833
+ end
834
+ end
835
+ @result.success?
836
+ end
837
+
838
+ # Modifies the attribute values of a particular entry on the LDAP
839
+ # directory. Takes a hash with arguments. Supported arguments are:
840
+ # :dn :: (the full DN of the entry whose attributes are to be modified)
841
+ # :operations :: (the modifications to be performed, detailed next)
842
+ #
843
+ # This method returns True or False to indicate whether the operation
844
+ # succeeded or failed, with extended information available by calling
845
+ # #get_operation_result.
846
+ #
847
+ # Also see #add_attribute, #replace_attribute, or #delete_attribute, which
848
+ # provide simpler interfaces to this functionality.
849
+ #
850
+ # The LDAP protocol provides a full and well thought-out set of operations
851
+ # for changing the values of attributes, but they are necessarily somewhat
852
+ # complex and not always intuitive. If these instructions are confusing or
853
+ # incomplete, please send us email or create a bug report on rubyforge.
854
+ #
855
+ # The :operations parameter to #modify takes an array of
856
+ # operation-descriptors. Each individual operation is specified in one
857
+ # element of the array, and most LDAP servers will attempt to perform the
858
+ # operations in order.
859
+ #
860
+ # Each of the operations appearing in the Array must itself be an Array
861
+ # with exactly three elements: an operator:: must be :add, :replace, or
862
+ # :delete an attribute name:: the attribute name (string or symbol) to
863
+ # modify a value:: either a string or an array of strings.
864
+ #
865
+ # The :add operator will, unsurprisingly, add the specified values to the
866
+ # specified attribute. If the attribute does not already exist, :add will
867
+ # create it. Most LDAP servers will generate an error if you try to add a
868
+ # value that already exists.
869
+ #
870
+ # :replace will erase the current value(s) for the specified attribute, if
871
+ # there are any, and replace them with the specified value(s).
872
+ #
873
+ # :delete will remove the specified value(s) from the specified attribute.
874
+ # If you pass nil, an empty string, or an empty array as the value
875
+ # parameter to a :delete operation, the _entire_ _attribute_ will be
876
+ # deleted, along with all of its values.
877
+ #
878
+ # For example:
879
+ #
880
+ # dn = "mail=modifyme@example.com, ou=people, dc=example, dc=com"
881
+ # ops = [
882
+ # [:add, :mail, "aliasaddress@example.com"],
883
+ # [:replace, :mail, ["newaddress@example.com", "newalias@example.com"]],
884
+ # [:delete, :sn, nil]
885
+ # ]
886
+ # ldap.modify :dn => dn, :operations => ops
887
+ #
888
+ # <i>(This example is contrived since you probably wouldn't add a mail
889
+ # value right before replacing the whole attribute, but it shows that
890
+ # order of execution matters. Also, many LDAP servers won't let you delete
891
+ # SN because that would be a schema violation.)</i>
892
+ #
893
+ # It's essential to keep in mind that if you specify more than one
894
+ # operation in a call to #modify, most LDAP servers will attempt to
895
+ # perform all of the operations in the order you gave them. This matters
896
+ # because you may specify operations on the same attribute which must be
897
+ # performed in a certain order.
898
+ #
899
+ # Most LDAP servers will _stop_ processing your modifications if one of
900
+ # them causes an error on the server (such as a schema-constraint
901
+ # violation). If this happens, you will probably get a result code from
902
+ # the server that reflects only the operation that failed, and you may or
903
+ # may not get extended information that will tell you which one failed.
904
+ # #modify has no notion of an atomic transaction. If you specify a chain
905
+ # of modifications in one call to #modify, and one of them fails, the
906
+ # preceding ones will usually not be "rolled back, " resulting in a
907
+ # partial update. This is a limitation of the LDAP protocol, not of
908
+ # Net::LDAP.
909
+ #
910
+ # The lack of transactional atomicity in LDAP means that you're usually
911
+ # better off using the convenience methods #add_attribute,
912
+ # #replace_attribute, and #delete_attribute, which are are wrappers over
913
+ # #modify. However, certain LDAP servers may provide concurrency
914
+ # semantics, in which the several operations contained in a single #modify
915
+ # call are not interleaved with other modification-requests received
916
+ # simultaneously by the server. It bears repeating that this concurrency
917
+ # does _not_ imply transactional atomicity, which LDAP does not provide.
918
+ def modify(args)
919
+ if @open_connection
920
+ @result = @open_connection.modify(args)
921
+ else
922
+ @result = 0
923
+ begin
924
+ conn = Connection.new(:host => @host, :port => @port,
925
+ :encryption => @encryption)
926
+ if (@result = conn.bind(args[:auth] || @auth)).result_code == 0
927
+ @result = conn.modify(args)
928
+ end
929
+ ensure
930
+ conn.close if conn
931
+ end
932
+ end
933
+
934
+ @result.success?
935
+ end
936
+
937
+ # Add a value to an attribute. Takes the full DN of the entry to modify,
938
+ # the name (Symbol or String) of the attribute, and the value (String or
939
+ # Array). If the attribute does not exist (and there are no schema
940
+ # violations), #add_attribute will create it with the caller-specified
941
+ # values. If the attribute already exists (and there are no schema
942
+ # violations), the caller-specified values will be _added_ to the values
943
+ # already present.
944
+ #
945
+ # Returns True or False to indicate whether the operation succeeded or
946
+ # failed, with extended information available by calling
947
+ # #get_operation_result. See also #replace_attribute and
948
+ # #delete_attribute.
949
+ #
950
+ # dn = "cn=modifyme, dc=example, dc=com"
951
+ # ldap.add_attribute dn, :mail, "newmailaddress@example.com"
952
+ def add_attribute(dn, attribute, value)
953
+ modify(:dn => dn, :operations => [[:add, attribute, value]])
954
+ end
955
+
956
+ # Replace the value of an attribute. #replace_attribute can be thought of
957
+ # as equivalent to calling #delete_attribute followed by #add_attribute.
958
+ # It takes the full DN of the entry to modify, the name (Symbol or String)
959
+ # of the attribute, and the value (String or Array). If the attribute does
960
+ # not exist, it will be created with the caller-specified value(s). If the
961
+ # attribute does exist, its values will be _discarded_ and replaced with
962
+ # the caller-specified values.
963
+ #
964
+ # Returns True or False to indicate whether the operation succeeded or
965
+ # failed, with extended information available by calling
966
+ # #get_operation_result. See also #add_attribute and #delete_attribute.
967
+ #
968
+ # dn = "cn=modifyme, dc=example, dc=com"
969
+ # ldap.replace_attribute dn, :mail, "newmailaddress@example.com"
970
+ def replace_attribute(dn, attribute, value)
971
+ modify(:dn => dn, :operations => [[:replace, attribute, value]])
972
+ end
973
+
974
+ # Delete an attribute and all its values. Takes the full DN of the entry
975
+ # to modify, and the name (Symbol or String) of the attribute to delete.
976
+ #
977
+ # Returns True or False to indicate whether the operation succeeded or
978
+ # failed, with extended information available by calling
979
+ # #get_operation_result. See also #add_attribute and #replace_attribute.
980
+ #
981
+ # dn = "cn=modifyme, dc=example, dc=com"
982
+ # ldap.delete_attribute dn, :mail
983
+ def delete_attribute(dn, attribute, value=nil)
984
+ modify(:dn => dn, :operations => [[:delete, attribute, value]])
985
+ end
986
+
987
+ # Rename an entry on the remote DIS by changing the last RDN of its DN.
988
+ #
989
+ # _Documentation_ _stub_
990
+ def rename(args)
991
+ if @open_connection
992
+ @result = @open_connection.rename(args)
993
+ else
994
+ @result = 0
995
+ begin
996
+ conn = Connection.new(:host => @host, :port => @port,
997
+ :encryption => @encryption)
998
+ if (@result = conn.bind(args[:auth] || @auth)).result_code == 0
999
+ @result = conn.rename(args)
1000
+ end
1001
+ ensure
1002
+ conn.close if conn
1003
+ end
1004
+ end
1005
+ @result.success?
1006
+ end
1007
+ alias_method :modify_rdn, :rename
1008
+
1009
+ # Delete an entry from the LDAP directory. Takes a hash of arguments. The
1010
+ # only supported argument is :dn, which must give the complete DN of the
1011
+ # entry to be deleted.
1012
+ #
1013
+ # Returns True or False to indicate whether the delete succeeded. Extended
1014
+ # status information is available by calling #get_operation_result.
1015
+ #
1016
+ # dn = "mail=deleteme@example.com, ou=people, dc=example, dc=com"
1017
+ # ldap.delete :dn => dn
1018
+ def delete(args)
1019
+ if @open_connection
1020
+ @result = @open_connection.delete(args)
1021
+ else
1022
+ @result = 0
1023
+ begin
1024
+ conn = Connection.new(:host => @host, :port => @port,
1025
+ :encryption => @encryption)
1026
+ if (@result = conn.bind(args[:auth] || @auth)).result_code == 0
1027
+ @result = conn.delete(args)
1028
+ end
1029
+ ensure
1030
+ conn.close
1031
+ end
1032
+ end
1033
+ @result.success?
1034
+ end
1035
+
1036
+ # Delete an entry from the LDAP directory along with all subordinate entries.
1037
+ # the regular delete method will fail to delete an entry if it has subordinate
1038
+ # entries. This method sends an extra control code to tell the LDAP server
1039
+ # to do a tree delete. ('1.2.840.113556.1.4.805')
1040
+ #
1041
+ # Returns True or False to indicate whether the delete succeeded. Extended
1042
+ # status information is available by calling #get_operation_result.
1043
+ #
1044
+ # dn = "mail=deleteme@example.com, ou=people, dc=example, dc=com"
1045
+ # ldap.delete_tree :dn => dn
1046
+ def delete_tree(args)
1047
+ delete(args.merge(:control_codes => [[Net::LDAP::LDAPControls::DELETE_TREE, true]]))
1048
+ end
1049
+ # This method is experimental and subject to change. Return the rootDSE
1050
+ # record from the LDAP server as a Net::LDAP::Entry, or an empty Entry if
1051
+ # the server doesn't return the record.
1052
+ #--
1053
+ # cf. RFC4512 graf 5.1.
1054
+ # Note that the rootDSE record we return on success has an empty DN, which
1055
+ # is correct. On failure, the empty Entry will have a nil DN. There's no
1056
+ # real reason for that, so it can be changed if desired. The funky
1057
+ # number-disagreements in the set of attribute names is correct per the
1058
+ # RFC. We may be called by #search itself, which may need to determine
1059
+ # things like paged search capabilities. So to avoid an infinite regress,
1060
+ # set :ignore_server_caps, which prevents us getting called recursively.
1061
+ #++
1062
+ def search_root_dse
1063
+ rs = search(:ignore_server_caps => true, :base => "",
1064
+ :scope => SearchScope_BaseObject,
1065
+ :attributes => [ :namingContexts, :supportedLdapVersion,
1066
+ :altServer, :supportedControl, :supportedExtension,
1067
+ :supportedFeatures, :supportedSASLMechanisms])
1068
+ (rs and rs.first) or Net::LDAP::Entry.new
1069
+ end
1070
+
1071
+ # Return the root Subschema record from the LDAP server as a
1072
+ # Net::LDAP::Entry, or an empty Entry if the server doesn't return the
1073
+ # record. On success, the Net::LDAP::Entry returned from this call will
1074
+ # have the attributes :dn, :objectclasses, and :attributetypes. If there
1075
+ # is an error, call #get_operation_result for more information.
1076
+ #
1077
+ # ldap = Net::LDAP.new
1078
+ # ldap.host = "your.ldap.host"
1079
+ # ldap.auth "your-user-dn", "your-psw"
1080
+ # subschema_entry = ldap.search_subschema_entry
1081
+ #
1082
+ # subschema_entry.attributetypes.each do |attrtype|
1083
+ # # your code
1084
+ # end
1085
+ #
1086
+ # subschema_entry.objectclasses.each do |attrtype|
1087
+ # # your code
1088
+ # end
1089
+ #--
1090
+ # cf. RFC4512 section 4, particulary graff 4.4.
1091
+ # The :dn attribute in the returned Entry is the subschema name as
1092
+ # returned from the server. Set :ignore_server_caps, see the notes in
1093
+ # search_root_dse.
1094
+ #++
1095
+ def search_subschema_entry
1096
+ rs = search(:ignore_server_caps => true, :base => "",
1097
+ :scope => SearchScope_BaseObject,
1098
+ :attributes => [:subschemaSubentry])
1099
+ return Net::LDAP::Entry.new unless (rs and rs.first)
1100
+
1101
+ subschema_name = rs.first.subschemasubentry
1102
+ return Net::LDAP::Entry.new unless (subschema_name and subschema_name.first)
1103
+
1104
+ rs = search(:ignore_server_caps => true, :base => subschema_name.first,
1105
+ :scope => SearchScope_BaseObject,
1106
+ :filter => "objectclass=subschema",
1107
+ :attributes => [:objectclasses, :attributetypes])
1108
+ (rs and rs.first) or Net::LDAP::Entry.new
1109
+ end
1110
+
1111
+ #--
1112
+ # Convenience method to query server capabilities.
1113
+ # Only do this once per Net::LDAP object.
1114
+ # Note, we call a search, and we might be called from inside a search!
1115
+ # MUST refactor the root_dse call out.
1116
+ #++
1117
+ def paged_searches_supported?
1118
+ # active directory returns that it supports paged results. However
1119
+ # it returns binary data in the rfc2696_cookie which throws an
1120
+ # encoding exception breaking searching.
1121
+ return false if @force_no_page
1122
+ @server_caps ||= search_root_dse
1123
+ @server_caps[:supportedcontrol].include?(Net::LDAP::LDAPControls::PAGED_RESULTS)
1124
+ end
1125
+ end # class LDAP
1126
+
1127
+ # This is a private class used internally by the library. It should not
1128
+ # be called by user code.
1129
+ class Net::LDAP::Connection #:nodoc:
1130
+ LdapVersion = 3
1131
+ MaxSaslChallenges = 10
1132
+
1133
+ def initialize(server)
1134
+ begin
1135
+ @conn = TCPSocket.new(server[:host], server[:port])
1136
+ rescue SocketError
1137
+ raise Net::LDAP::LdapError, "No such address or other socket error."
1138
+ rescue Errno::ECONNREFUSED
1139
+ raise Net::LDAP::LdapError, "Server #{server[:host]} refused connection on port #{server[:port]}."
1140
+ end
1141
+
1142
+ if server[:encryption]
1143
+ setup_encryption server[:encryption]
1144
+ end
1145
+
1146
+ yield self if block_given?
1147
+ end
1148
+
1149
+ module GetbyteForSSLSocket
1150
+ def getbyte
1151
+ getc.ord
1152
+ end
1153
+ end
1154
+
1155
+ def self.wrap_with_ssl(io)
1156
+ raise Net::LDAP::LdapError, "OpenSSL is unavailable" unless Net::LDAP::HasOpenSSL
1157
+ ctx = OpenSSL::SSL::SSLContext.new
1158
+ conn = OpenSSL::SSL::SSLSocket.new(io, ctx)
1159
+ conn.connect
1160
+ conn.sync_close = true
1161
+
1162
+ conn.extend(GetbyteForSSLSocket) unless conn.respond_to?(:getbyte)
1163
+
1164
+ conn
1165
+ end
1166
+
1167
+ #--
1168
+ # Helper method called only from new, and only after we have a
1169
+ # successfully-opened @conn instance variable, which is a TCP connection.
1170
+ # Depending on the received arguments, we establish SSL, potentially
1171
+ # replacing the value of @conn accordingly. Don't generate any errors here
1172
+ # if no encryption is requested. DO raise Net::LDAP::LdapError objects if encryption
1173
+ # is requested and we have trouble setting it up. That includes if OpenSSL
1174
+ # is not set up on the machine. (Question: how does the Ruby OpenSSL
1175
+ # wrapper react in that case?) DO NOT filter exceptions raised by the
1176
+ # OpenSSL library. Let them pass back to the user. That should make it
1177
+ # easier for us to debug the problem reports. Presumably (hopefully?) that
1178
+ # will also produce recognizable errors if someone tries to use this on a
1179
+ # machine without OpenSSL.
1180
+ #
1181
+ # The simple_tls method is intended as the simplest, stupidest, easiest
1182
+ # solution for people who want nothing more than encrypted comms with the
1183
+ # LDAP server. It doesn't do any server-cert validation and requires
1184
+ # nothing in the way of key files and root-cert files, etc etc. OBSERVE:
1185
+ # WE REPLACE the value of @conn, which is presumed to be a connected
1186
+ # TCPSocket object.
1187
+ #
1188
+ # The start_tls method is supported by many servers over the standard LDAP
1189
+ # port. It does not require an alternative port for encrypted
1190
+ # communications, as with simple_tls. Thanks for Kouhei Sutou for
1191
+ # generously contributing the :start_tls path.
1192
+ #++
1193
+ def setup_encryption(args)
1194
+ case args[:method]
1195
+ when :simple_tls
1196
+ @conn = self.class.wrap_with_ssl(@conn)
1197
+ # additional branches requiring server validation and peer certs, etc.
1198
+ # go here.
1199
+ when :start_tls
1200
+ msgid = next_msgid.to_ber
1201
+ request = [Net::LDAP::StartTlsOid.to_ber].to_ber_appsequence(Net::LDAP::PDU::ExtendedRequest)
1202
+ request_pkt = [msgid, request].to_ber_sequence
1203
+ @conn.write request_pkt
1204
+ be = @conn.read_ber(Net::LDAP::AsnSyntax)
1205
+ raise Net::LDAP::LdapError, "no start_tls result" if be.nil?
1206
+ pdu = Net::LDAP::PDU.new(be)
1207
+ raise Net::LDAP::LdapError, "no start_tls result" if pdu.nil?
1208
+ if pdu.result_code.zero?
1209
+ @conn = self.class.wrap_with_ssl(@conn)
1210
+ else
1211
+ raise Net::LDAP::LdapError, "start_tls failed: #{pdu.result_code}"
1212
+ end
1213
+ else
1214
+ raise Net::LDAP::LdapError, "unsupported encryption method #{args[:method]}"
1215
+ end
1216
+ end
1217
+
1218
+ #--
1219
+ # This is provided as a convenience method to make sure a connection
1220
+ # object gets closed without waiting for a GC to happen. Clients shouldn't
1221
+ # have to call it, but perhaps it will come in handy someday.
1222
+ #++
1223
+ def close
1224
+ @conn.close
1225
+ @conn = nil
1226
+ end
1227
+
1228
+ def next_msgid
1229
+ @msgid ||= 0
1230
+ @msgid += 1
1231
+ end
1232
+
1233
+ def bind(auth)
1234
+ meth = auth[:method]
1235
+ if [:simple, :anonymous, :anon].include?(meth)
1236
+ bind_simple auth
1237
+ elsif meth == :sasl
1238
+ bind_sasl(auth)
1239
+ elsif meth == :gss_spnego
1240
+ bind_gss_spnego(auth)
1241
+ else
1242
+ raise Net::LDAP::LdapError, "Unsupported auth method (#{meth})"
1243
+ end
1244
+ end
1245
+
1246
+ #--
1247
+ # Implements a simple user/psw authentication. Accessed by calling #bind
1248
+ # with a method of :simple or :anonymous.
1249
+ #++
1250
+ def bind_simple(auth)
1251
+ user, psw = if auth[:method] == :simple
1252
+ [auth[:username] || auth[:dn], auth[:password]]
1253
+ else
1254
+ ["", ""]
1255
+ end
1256
+
1257
+ raise Net::LDAP::LdapError, "Invalid binding information" unless (user && psw)
1258
+
1259
+ msgid = next_msgid.to_ber
1260
+ request = [LdapVersion.to_ber, user.to_ber,
1261
+ psw.to_ber_contextspecific(0)].to_ber_appsequence(0)
1262
+ request_pkt = [msgid, request].to_ber_sequence
1263
+ @conn.write request_pkt
1264
+
1265
+ (be = @conn.read_ber(Net::LDAP::AsnSyntax) and pdu = Net::LDAP::PDU.new(be)) or raise Net::LDAP::LdapError, "no bind result"
1266
+
1267
+ pdu
1268
+ end
1269
+
1270
+ #--
1271
+ # Required parameters: :mechanism, :initial_credential and
1272
+ # :challenge_response
1273
+ #
1274
+ # Mechanism is a string value that will be passed in the SASL-packet's
1275
+ # "mechanism" field.
1276
+ #
1277
+ # Initial credential is most likely a string. It's passed in the initial
1278
+ # BindRequest that goes to the server. In some protocols, it may be empty.
1279
+ #
1280
+ # Challenge-response is a Ruby proc that takes a single parameter and
1281
+ # returns an object that will typically be a string. The
1282
+ # challenge-response block is called when the server returns a
1283
+ # BindResponse with a result code of 14 (saslBindInProgress). The
1284
+ # challenge-response block receives a parameter containing the data
1285
+ # returned by the server in the saslServerCreds field of the LDAP
1286
+ # BindResponse packet. The challenge-response block may be called multiple
1287
+ # times during the course of a SASL authentication, and each time it must
1288
+ # return a value that will be passed back to the server as the credential
1289
+ # data in the next BindRequest packet.
1290
+ #++
1291
+ def bind_sasl(auth)
1292
+ mech, cred, chall = auth[:mechanism], auth[:initial_credential],
1293
+ auth[:challenge_response]
1294
+ raise Net::LDAP::LdapError, "Invalid binding information" unless (mech && cred && chall)
1295
+
1296
+ n = 0
1297
+ loop {
1298
+ msgid = next_msgid.to_ber
1299
+ sasl = [mech.to_ber, cred.to_ber].to_ber_contextspecific(3)
1300
+ request = [LdapVersion.to_ber, "".to_ber, sasl].to_ber_appsequence(0)
1301
+ request_pkt = [msgid, request].to_ber_sequence
1302
+ @conn.write request_pkt
1303
+
1304
+ (be = @conn.read_ber(Net::LDAP::AsnSyntax) and pdu = Net::LDAP::PDU.new(be)) or raise Net::LDAP::LdapError, "no bind result"
1305
+ return pdu unless pdu.result_code == 14 # saslBindInProgress
1306
+ raise Net::LDAP::LdapError, "sasl-challenge overflow" if ((n += 1) > MaxSaslChallenges)
1307
+
1308
+ cred = chall.call(pdu.result_server_sasl_creds)
1309
+ }
1310
+
1311
+ raise Net::LDAP::LdapError, "why are we here?"
1312
+ end
1313
+ private :bind_sasl
1314
+
1315
+ #--
1316
+ # PROVISIONAL, only for testing SASL implementations. DON'T USE THIS YET.
1317
+ # Uses Kohei Kajimoto's Ruby/NTLM. We have to find a clean way to
1318
+ # integrate it without introducing an external dependency.
1319
+ #
1320
+ # This authentication method is accessed by calling #bind with a :method
1321
+ # parameter of :gss_spnego. It requires :username and :password
1322
+ # attributes, just like the :simple authentication method. It performs a
1323
+ # GSS-SPNEGO authentication with the server, which is presumed to be a
1324
+ # Microsoft Active Directory.
1325
+ #++
1326
+ def bind_gss_spnego(auth)
1327
+ require 'ntlm'
1328
+
1329
+ user, psw = [auth[:username] || auth[:dn], auth[:password]]
1330
+ raise Net::LDAP::LdapError, "Invalid binding information" unless (user && psw)
1331
+
1332
+ nego = proc { |challenge|
1333
+ t2_msg = NTLM::Message.parse(challenge)
1334
+ t3_msg = t2_msg.response({ :user => user, :password => psw },
1335
+ { :ntlmv2 => true })
1336
+ t3_msg.serialize
1337
+ }
1338
+
1339
+ bind_sasl(:method => :sasl, :mechanism => "GSS-SPNEGO",
1340
+ :initial_credential => NTLM::Message::Type1.new.serialize,
1341
+ :challenge_response => nego)
1342
+ end
1343
+ private :bind_gss_spnego
1344
+
1345
+
1346
+ #--
1347
+ # Allow the caller to specify a sort control
1348
+ #
1349
+ # The format of the sort control needs to be:
1350
+ #
1351
+ # :sort_control => ["cn"] # just a string
1352
+ # or
1353
+ # :sort_control => [["cn", "matchingRule", true]] #attribute, matchingRule, direction (true / false)
1354
+ # or
1355
+ # :sort_control => ["givenname","sn"] #multiple strings or arrays
1356
+ #
1357
+ def encode_sort_controls(sort_definitions)
1358
+ return sort_definitions unless sort_definitions
1359
+
1360
+ sort_control_values = sort_definitions.map do |control|
1361
+ control = Array(control) # if there is only an attribute name as a string then infer the orderinrule and reverseorder
1362
+ control[0] = String(control[0]).to_ber,
1363
+ control[1] = String(control[1]).to_ber,
1364
+ control[2] = (control[2] == true).to_ber
1365
+ control.to_ber_sequence
1366
+ end
1367
+ sort_control = [
1368
+ Net::LDAP::LDAPControls::SORT_REQUEST.to_ber,
1369
+ false.to_ber,
1370
+ sort_control_values.to_ber_sequence.to_s.to_ber
1371
+ ].to_ber_sequence
1372
+ end
1373
+
1374
+ #--
1375
+ # Alternate implementation, this yields each search entry to the caller as
1376
+ # it are received.
1377
+ #
1378
+ # TODO: certain search parameters are hardcoded.
1379
+ # TODO: if we mis-parse the server results or the results are wrong, we
1380
+ # can block forever. That's because we keep reading results until we get a
1381
+ # type-5 packet, which might never come. We need to support the time-limit
1382
+ # in the protocol.
1383
+ #++
1384
+ def search(args = {})
1385
+ search_filter = (args && args[:filter]) ||
1386
+ Net::LDAP::Filter.eq("objectclass", "*")
1387
+ search_filter = Net::LDAP::Filter.construct(search_filter) if search_filter.is_a?(String)
1388
+ search_base = (args && args[:base]) || "dc=example, dc=com"
1389
+ search_attributes = ((args && args[:attributes]) || []).map { |attr| attr.to_s.to_ber}
1390
+ return_referrals = args && args[:return_referrals] == true
1391
+ sizelimit = (args && args[:size].to_i) || 0
1392
+ raise Net::LDAP::LdapError, "invalid search-size" unless sizelimit >= 0
1393
+ paged_searches_supported = (args && args[:paged_searches_supported])
1394
+
1395
+ attributes_only = (args and args[:attributes_only] == true)
1396
+ scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree
1397
+ raise Net::LDAP::LdapError, "invalid search scope" unless Net::LDAP::SearchScopes.include?(scope)
1398
+
1399
+ sort_control = encode_sort_controls(args.fetch(:sort_controls){ false })
1400
+ # An interesting value for the size limit would be close to A/D's
1401
+ # built-in page limit of 1000 records, but openLDAP newer than version
1402
+ # 2.2.0 chokes on anything bigger than 126. You get a silent error that
1403
+ # is easily visible by running slapd in debug mode. Go figure.
1404
+ #
1405
+ # Changed this around 06Sep06 to support a caller-specified search-size
1406
+ # limit. Because we ALWAYS do paged searches, we have to work around the
1407
+ # problem that it's not legal to specify a "normal" sizelimit (in the
1408
+ # body of the search request) that is larger than the page size we're
1409
+ # requesting. Unfortunately, I have the feeling that this will break
1410
+ # with LDAP servers that don't support paged searches!!!
1411
+ #
1412
+ # (Because we pass zero as the sizelimit on search rounds when the
1413
+ # remaining limit is larger than our max page size of 126. In these
1414
+ # cases, I think the caller's search limit will be ignored!)
1415
+ #
1416
+ # CONFIRMED: This code doesn't work on LDAPs that don't support paged
1417
+ # searches when the size limit is larger than 126. We're going to have
1418
+ # to do a root-DSE record search and not do a paged search if the LDAP
1419
+ # doesn't support it. Yuck.
1420
+ rfc2696_cookie = [126, ""]
1421
+ result_pdu = nil
1422
+ n_results = 0
1423
+
1424
+ loop {
1425
+ # should collect this into a private helper to clarify the structure
1426
+ query_limit = 0
1427
+ if sizelimit > 0
1428
+ if paged_searches_supported
1429
+ query_limit = (((sizelimit - n_results) < 126) ? (sizelimit -
1430
+ n_results) : 0)
1431
+ else
1432
+ query_limit = sizelimit
1433
+ end
1434
+ end
1435
+
1436
+ request = [
1437
+ search_base.to_ber,
1438
+ scope.to_ber_enumerated,
1439
+ 0.to_ber_enumerated,
1440
+ query_limit.to_ber, # size limit
1441
+ 0.to_ber,
1442
+ attributes_only.to_ber,
1443
+ search_filter.to_ber,
1444
+ search_attributes.to_ber_sequence
1445
+ ].to_ber_appsequence(3)
1446
+
1447
+ # rfc2696_cookie sometimes contains binary data from Microsoft Active Directory
1448
+ # this breaks when calling to_ber. (Can't force binary data to UTF-8)
1449
+ # we have to disable paging (even though server supports it) to get around this...
1450
+
1451
+ controls = []
1452
+ controls <<
1453
+ [
1454
+ Net::LDAP::LDAPControls::PAGED_RESULTS.to_ber,
1455
+ # Criticality MUST be false to interoperate with normal LDAPs.
1456
+ false.to_ber,
1457
+ rfc2696_cookie.map{ |v| v.to_ber}.to_ber_sequence.to_s.to_ber
1458
+ ].to_ber_sequence if paged_searches_supported
1459
+ controls << sort_control if sort_control
1460
+ controls = controls.empty? ? nil : controls.to_ber_contextspecific(0)
1461
+
1462
+ pkt = [next_msgid.to_ber, request, controls].compact.to_ber_sequence
1463
+ @conn.write pkt
1464
+
1465
+ result_pdu = nil
1466
+ controls = []
1467
+
1468
+ while (be = @conn.read_ber(Net::LDAP::AsnSyntax)) && (pdu = Net::LDAP::PDU.new(be))
1469
+ case pdu.app_tag
1470
+ when 4 # search-data
1471
+ n_results += 1
1472
+ yield pdu.search_entry if block_given?
1473
+ when 19 # search-referral
1474
+ if return_referrals
1475
+ if block_given?
1476
+ se = Net::LDAP::Entry.new
1477
+ se[:search_referrals] = (pdu.search_referrals || [])
1478
+ yield se
1479
+ end
1480
+ end
1481
+ when 5 # search-result
1482
+ result_pdu = pdu
1483
+ controls = pdu.result_controls
1484
+ if return_referrals && result_code == 10
1485
+ if block_given?
1486
+ se = Net::LDAP::Entry.new
1487
+ se[:search_referrals] = (pdu.search_referrals || [])
1488
+ yield se
1489
+ end
1490
+ end
1491
+ break
1492
+ else
1493
+ raise Net::LDAP::LdapError, "invalid response-type in search: #{pdu.app_tag}"
1494
+ end
1495
+ end
1496
+
1497
+ # When we get here, we have seen a type-5 response. If there is no
1498
+ # error AND there is an RFC-2696 cookie, then query again for the next
1499
+ # page of results. If not, we're done. Don't screw this up or we'll
1500
+ # break every search we do.
1501
+ #
1502
+ # Noticed 02Sep06, look at the read_ber call in this loop, shouldn't
1503
+ # that have a parameter of AsnSyntax? Does this just accidentally
1504
+ # work? According to RFC-2696, the value expected in this position is
1505
+ # of type OCTET STRING, covered in the default syntax supported by
1506
+ # read_ber, so I guess we're ok.
1507
+ more_pages = false
1508
+ if result_pdu.result_code == 0 and controls
1509
+ controls.each do |c|
1510
+ if c.oid == Net::LDAP::LDAPControls::PAGED_RESULTS
1511
+ # just in case some bogus server sends us more than 1 of these.
1512
+ more_pages = false
1513
+ if c.value and c.value.length > 0
1514
+ cookie = c.value.read_ber[1]
1515
+ if cookie and cookie.length > 0
1516
+ rfc2696_cookie[1] = cookie
1517
+ more_pages = true
1518
+ end
1519
+ end
1520
+ end
1521
+ end
1522
+ end
1523
+
1524
+ break unless more_pages
1525
+ } # loop
1526
+
1527
+ result_pdu || OpenStruct.new(:status => :failure, :result_code => 1, :message => "Invalid search")
1528
+ end
1529
+
1530
+ MODIFY_OPERATIONS = { #:nodoc:
1531
+ :add => 0,
1532
+ :delete => 1,
1533
+ :replace => 2
1534
+ }
1535
+
1536
+ def self.modify_ops(operations)
1537
+ ops = []
1538
+ if operations
1539
+ operations.each { |op, attrib, values|
1540
+ # TODO, fix the following line, which gives a bogus error if the
1541
+ # opcode is invalid.
1542
+ op_ber = MODIFY_OPERATIONS[op.to_sym].to_ber_enumerated
1543
+ values = [ values ].flatten.map { |v| v.to_ber if v }.to_ber_set
1544
+ values = [ attrib.to_s.to_ber, values ].to_ber_sequence
1545
+ ops << [ op_ber, values ].to_ber
1546
+ }
1547
+ end
1548
+ ops
1549
+ end
1550
+
1551
+ #--
1552
+ # TODO: need to support a time limit, in case the server fails to respond.
1553
+ # TODO: We're throwing an exception here on empty DN. Should return a
1554
+ # proper error instead, probaby from farther up the chain.
1555
+ # TODO: If the user specifies a bogus opcode, we'll throw a confusing
1556
+ # error here ("to_ber_enumerated is not defined on nil").
1557
+ #++
1558
+ def modify(args)
1559
+ modify_dn = args[:dn] or raise "Unable to modify empty DN"
1560
+ ops = self.class.modify_ops args[:operations]
1561
+ request = [ modify_dn.to_ber,
1562
+ ops.to_ber_sequence ].to_ber_appsequence(6)
1563
+ pkt = [ next_msgid.to_ber, request ].to_ber_sequence
1564
+ @conn.write pkt
1565
+
1566
+ (be = @conn.read_ber(Net::LDAP::AsnSyntax)) && (pdu = Net::LDAP::PDU.new(be)) && (pdu.app_tag == 7) or raise Net::LDAP::LdapError, "response missing or invalid"
1567
+
1568
+ pdu
1569
+ end
1570
+
1571
+ #--
1572
+ # TODO: need to support a time limit, in case the server fails to respond.
1573
+ # Unlike other operation-methods in this class, we return a result hash
1574
+ # rather than a simple result number. This is experimental, and eventually
1575
+ # we'll want to do this with all the others. The point is to have access
1576
+ # to the error message and the matched-DN returned by the server.
1577
+ #++
1578
+ def add(args)
1579
+ add_dn = args[:dn] or raise Net::LDAP::LdapError, "Unable to add empty DN"
1580
+ add_attrs = []
1581
+ a = args[:attributes] and a.each { |k, v|
1582
+ add_attrs << [ k.to_s.to_ber, Array(v).map { |m| m.to_ber}.to_ber_set ].to_ber_sequence
1583
+ }
1584
+
1585
+ request = [add_dn.to_ber, add_attrs.to_ber_sequence].to_ber_appsequence(8)
1586
+ pkt = [next_msgid.to_ber, request].to_ber_sequence
1587
+ @conn.write pkt
1588
+
1589
+ (be = @conn.read_ber(Net::LDAP::AsnSyntax)) &&
1590
+ (pdu = Net::LDAP::PDU.new(be)) &&
1591
+ (pdu.app_tag == 9) or
1592
+ raise Net::LDAP::LdapError, "response missing or invalid"
1593
+
1594
+ pdu
1595
+ end
1596
+
1597
+ #--
1598
+ # TODO: need to support a time limit, in case the server fails to respond.
1599
+ #++
1600
+ def rename(args)
1601
+ old_dn = args[:olddn] or raise "Unable to rename empty DN"
1602
+ new_rdn = args[:newrdn] or raise "Unable to rename to empty RDN"
1603
+ delete_attrs = args[:delete_attributes] ? true : false
1604
+ new_superior = args[:new_superior]
1605
+
1606
+ request = [old_dn.to_ber, new_rdn.to_ber, delete_attrs.to_ber]
1607
+ request << new_superior.to_ber_contextspecific(0) unless new_superior == nil
1608
+
1609
+ pkt = [next_msgid.to_ber, request.to_ber_appsequence(12)].to_ber_sequence
1610
+ @conn.write pkt
1611
+
1612
+ (be = @conn.read_ber(Net::LDAP::AsnSyntax)) &&
1613
+ (pdu = Net::LDAP::PDU.new( be )) && (pdu.app_tag == 13) or
1614
+ raise Net::LDAP::LdapError.new( "response missing or invalid" )
1615
+
1616
+ pdu
1617
+ end
1618
+
1619
+ #--
1620
+ # TODO, need to support a time limit, in case the server fails to respond.
1621
+ #++
1622
+ def delete(args)
1623
+ dn = args[:dn] or raise "Unable to delete empty DN"
1624
+ controls = args.include?(:control_codes) ? args[:control_codes].to_ber_control : nil #use nil so we can compact later
1625
+ request = dn.to_s.to_ber_application_string(10)
1626
+ pkt = [next_msgid.to_ber, request, controls].compact.to_ber_sequence
1627
+ @conn.write pkt
1628
+
1629
+ (be = @conn.read_ber(Net::LDAP::AsnSyntax)) && (pdu = Net::LDAP::PDU.new(be)) && (pdu.app_tag == 11) or raise Net::LDAP::LdapError, "response missing or invalid"
1630
+
1631
+ pdu
1632
+ end
1633
+ end # class Connection