net-ldap 0.1.1 → 0.2

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.

Potentially problematic release.


This version of net-ldap might be problematic. Click here for more details.

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