datacom-net-ldap 0.5.0.datacom

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