rubinius-net-ldap 0.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rubocop.yml +5 -0
  4. data/.rubocop_todo.yml +462 -0
  5. data/.travis.yml +19 -0
  6. data/CONTRIBUTING.md +54 -0
  7. data/Contributors.rdoc +24 -0
  8. data/Gemfile +2 -0
  9. data/Hacking.rdoc +63 -0
  10. data/History.rdoc +260 -0
  11. data/License.rdoc +29 -0
  12. data/README.rdoc +65 -0
  13. data/Rakefile +17 -0
  14. data/lib/net-ldap.rb +2 -0
  15. data/lib/net/ber.rb +320 -0
  16. data/lib/net/ber/ber_parser.rb +182 -0
  17. data/lib/net/ber/core_ext.rb +55 -0
  18. data/lib/net/ber/core_ext/array.rb +96 -0
  19. data/lib/net/ber/core_ext/false_class.rb +10 -0
  20. data/lib/net/ber/core_ext/integer.rb +74 -0
  21. data/lib/net/ber/core_ext/string.rb +66 -0
  22. data/lib/net/ber/core_ext/true_class.rb +11 -0
  23. data/lib/net/ldap.rb +1229 -0
  24. data/lib/net/ldap/connection.rb +702 -0
  25. data/lib/net/ldap/dataset.rb +168 -0
  26. data/lib/net/ldap/dn.rb +225 -0
  27. data/lib/net/ldap/entry.rb +193 -0
  28. data/lib/net/ldap/error.rb +38 -0
  29. data/lib/net/ldap/filter.rb +778 -0
  30. data/lib/net/ldap/instrumentation.rb +23 -0
  31. data/lib/net/ldap/password.rb +38 -0
  32. data/lib/net/ldap/pdu.rb +297 -0
  33. data/lib/net/ldap/version.rb +5 -0
  34. data/lib/net/snmp.rb +264 -0
  35. data/rubinius-net-ldap.gemspec +37 -0
  36. data/script/install-openldap +112 -0
  37. data/script/package +7 -0
  38. data/script/release +16 -0
  39. data/test/ber/core_ext/test_array.rb +22 -0
  40. data/test/ber/core_ext/test_string.rb +25 -0
  41. data/test/ber/test_ber.rb +99 -0
  42. data/test/fixtures/cacert.pem +20 -0
  43. data/test/fixtures/openldap/memberof.ldif +33 -0
  44. data/test/fixtures/openldap/retcode.ldif +76 -0
  45. data/test/fixtures/openldap/slapd.conf.ldif +67 -0
  46. data/test/fixtures/seed.ldif +374 -0
  47. data/test/integration/test_add.rb +28 -0
  48. data/test/integration/test_ber.rb +30 -0
  49. data/test/integration/test_bind.rb +34 -0
  50. data/test/integration/test_delete.rb +31 -0
  51. data/test/integration/test_open.rb +88 -0
  52. data/test/integration/test_return_codes.rb +38 -0
  53. data/test/integration/test_search.rb +77 -0
  54. data/test/support/vm/openldap/.gitignore +1 -0
  55. data/test/support/vm/openldap/README.md +32 -0
  56. data/test/support/vm/openldap/Vagrantfile +33 -0
  57. data/test/test_dn.rb +44 -0
  58. data/test/test_entry.rb +65 -0
  59. data/test/test_filter.rb +223 -0
  60. data/test/test_filter_parser.rb +20 -0
  61. data/test/test_helper.rb +66 -0
  62. data/test/test_ldap.rb +60 -0
  63. data/test/test_ldap_connection.rb +404 -0
  64. data/test/test_ldif.rb +104 -0
  65. data/test/test_password.rb +10 -0
  66. data/test/test_rename.rb +77 -0
  67. data/test/test_search.rb +39 -0
  68. data/test/test_snmp.rb +119 -0
  69. data/test/test_ssl_ber.rb +40 -0
  70. data/test/testdata.ldif +101 -0
  71. data/testserver/ldapserver.rb +210 -0
  72. data/testserver/testdata.ldif +101 -0
  73. metadata +204 -0
@@ -0,0 +1,168 @@
1
+ # -*- ruby encoding: utf-8 -*-
2
+ ##
3
+ # An LDAP Dataset. Used primarily as an intermediate format for converting
4
+ # to and from LDIF strings and Net::LDAP::Entry objects.
5
+ class Net::LDAP::Dataset < Hash
6
+ ##
7
+ # Dataset object version, comments.
8
+ attr_accessor :version
9
+ attr_reader :comments
10
+
11
+ def initialize(*args, &block) # :nodoc:
12
+ super
13
+ @version = nil
14
+ @comments = []
15
+ end
16
+
17
+ ##
18
+ # Outputs an LDAP Dataset as an array of strings representing LDIF
19
+ # entries.
20
+ def to_ldif
21
+ ary = []
22
+
23
+ if version
24
+ ary << "version: #{version}"
25
+ ary << ""
26
+ end
27
+
28
+ ary += @comments unless @comments.empty?
29
+ keys.sort.each do |dn|
30
+ ary << "dn: #{dn}"
31
+
32
+ attributes = self[dn].keys.map { |attr| attr.to_s }.sort
33
+ attributes.each do |attr|
34
+ self[dn][attr.to_sym].each do |value|
35
+ if attr == "userpassword" or value_is_binary?(value)
36
+ value = [value].pack("m").chomp.gsub(/\n/m, "\n ")
37
+ ary << "#{attr}:: #{value}"
38
+ else
39
+ ary << "#{attr}: #{value}"
40
+ end
41
+ end
42
+ end
43
+
44
+ ary << ""
45
+ end
46
+ block_given? and ary.each { |line| yield line}
47
+
48
+ ary
49
+ end
50
+
51
+ ##
52
+ # Outputs an LDAP Dataset as an LDIF string.
53
+ def to_ldif_string
54
+ to_ldif.join("\n")
55
+ end
56
+
57
+ ##
58
+ # Convert the parsed LDIF objects to Net::LDAP::Entry objects.
59
+ def to_entries
60
+ ary = []
61
+ keys.each do |dn|
62
+ entry = Net::LDAP::Entry.new(dn)
63
+ self[dn].each do |attr, value|
64
+ entry[attr] = value
65
+ end
66
+ ary << entry
67
+ end
68
+ ary
69
+ end
70
+
71
+ ##
72
+ # This is an internal convenience method to determine if a value requires
73
+ # base64-encoding before conversion to LDIF output. The standard approach
74
+ # in most LDAP tools is to check whether the value is a password, or if
75
+ # the first or last bytes are non-printable. Microsoft Active Directory,
76
+ # on the other hand, sometimes sends values that are binary in the middle.
77
+ #
78
+ # In the worst cases, this could be a nasty performance killer, which is
79
+ # why we handle the simplest cases first. Ideally, we would also test the
80
+ # first/last byte, but it's a bit harder to do this in a way that's
81
+ # compatible with both 1.8.6 and 1.8.7.
82
+ def value_is_binary?(value) # :nodoc:
83
+ value = value.to_s
84
+ return true if value[0] == ?: or value[0] == ?<
85
+ value.each_byte { |byte| return true if (byte < 32) || (byte > 126) }
86
+ false
87
+ end
88
+ private :value_is_binary?
89
+
90
+ class << self
91
+ class ChompedIO # :nodoc:
92
+ def initialize(io)
93
+ @io = io
94
+ end
95
+ def gets
96
+ s = @io.gets
97
+ s.chomp if s
98
+ end
99
+ end
100
+
101
+ ##
102
+ # Creates a Dataset object from an Entry object. Used mostly to assist
103
+ # with the conversion of
104
+ def from_entry(entry)
105
+ dataset = Net::LDAP::Dataset.new
106
+ hash = { }
107
+ entry.each_attribute do |attribute, value|
108
+ next if attribute == :dn
109
+ hash[attribute] = value
110
+ end
111
+ dataset[entry.dn] = hash
112
+ dataset
113
+ end
114
+
115
+ ##
116
+ # Reads an object that returns data line-wise (using #gets) and parses
117
+ # LDIF data into a Dataset object.
118
+ def read_ldif(io)
119
+ ds = Net::LDAP::Dataset.new
120
+ io = ChompedIO.new(io)
121
+
122
+ line = io.gets
123
+ dn = nil
124
+
125
+ while line
126
+ new_line = io.gets
127
+
128
+ if new_line =~ /^ /
129
+ line << $'
130
+ else
131
+ nextline = new_line
132
+
133
+ if line =~ /^#/
134
+ ds.comments << line
135
+ yield :comment, line if block_given?
136
+ elsif line =~ /^version:[\s]*([0-9]+)$/i
137
+ ds.version = $1
138
+ yield :version, line if block_given?
139
+ elsif line =~ /^dn:([\:]?)[\s]*/i
140
+ # $1 is a colon if the dn-value is base-64 encoded
141
+ # $' is the dn-value
142
+ # Avoid the Base64 class because not all Ruby versions have it.
143
+ dn = ($1 == ":") ? $'.unpack('m').shift : $'
144
+ ds[dn] = Hash.new { |k,v| k[v] = [] }
145
+ yield :dn, dn if block_given?
146
+ elsif line.empty?
147
+ dn = nil
148
+ yield :end, nil if block_given?
149
+ elsif line =~ /^([^:]+):([\:]?)[\s]*/
150
+ # $1 is the attribute name
151
+ # $2 is a colon iff the attr-value is base-64 encoded
152
+ # $' is the attr-value
153
+ # Avoid the Base64 class because not all Ruby versions have it.
154
+ attrvalue = ($2 == ":") ? $'.unpack('m').shift : $'
155
+ ds[dn][$1.downcase.to_sym] << attrvalue
156
+ yield :attr, [$1.downcase.to_sym, attrvalue] if block_given?
157
+ end
158
+
159
+ line = nextline
160
+ end
161
+ end
162
+
163
+ ds
164
+ end
165
+ end
166
+ end
167
+
168
+ require 'net/ldap/entry' unless defined? Net::LDAP::Entry
@@ -0,0 +1,225 @@
1
+ # -*- ruby encoding: utf-8 -*-
2
+
3
+ ##
4
+ # Objects of this class represent an LDAP DN ("Distinguished Name"). A DN
5
+ # ("Distinguished Name") is a unique identifier for an entry within an LDAP
6
+ # directory. It is made up of a number of other attributes strung together,
7
+ # to identify the entry in the tree.
8
+ #
9
+ # Each attribute that makes up a DN needs to have its value escaped so that
10
+ # the DN is valid. This class helps take care of that.
11
+ #
12
+ # A fully escaped DN needs to be unescaped when analysing its contents. This
13
+ # class also helps take care of that.
14
+ class Net::LDAP::DN
15
+ ##
16
+ # Initialize a DN, escaping as required. Pass in attributes in name/value
17
+ # pairs. If there is a left over argument, it will be appended to the dn
18
+ # without escaping (useful for a base string).
19
+ #
20
+ # Most uses of this class will be to escape a DN, rather than to parse it,
21
+ # so storing the dn as an escaped String and parsing parts as required
22
+ # with a state machine seems sensible.
23
+ def initialize(*args)
24
+ buffer = StringIO.new
25
+
26
+ args.each_index do |index|
27
+ buffer << "=" if index % 2 == 1
28
+ buffer << "," if index % 2 == 0 && index != 0
29
+
30
+ if index < args.length - 1 || index % 2 == 1
31
+ buffer << Net::LDAP::DN.escape(args[index])
32
+ else
33
+ buffer << args[index]
34
+ end
35
+ end
36
+
37
+ @dn = buffer.string
38
+ end
39
+
40
+ ##
41
+ # Parse a DN into key value pairs using ASN from
42
+ # http://tools.ietf.org/html/rfc2253 section 3.
43
+ def each_pair
44
+ state = :key
45
+ key = StringIO.new
46
+ value = StringIO.new
47
+ hex_buffer = ""
48
+
49
+ @dn.each_char do |char|
50
+ case state
51
+ when :key then
52
+ case char
53
+ when 'a'..'z', 'A'..'Z' then
54
+ state = :key_normal
55
+ key << char
56
+ when '0'..'9' then
57
+ state = :key_oid
58
+ key << char
59
+ when ' ' then state = :key
60
+ else raise "DN badly formed"
61
+ end
62
+ when :key_normal then
63
+ case char
64
+ when '=' then state = :value
65
+ when 'a'..'z', 'A'..'Z', '0'..'9', '-', ' ' then key << char
66
+ else raise "DN badly formed"
67
+ end
68
+ when :key_oid then
69
+ case char
70
+ when '=' then state = :value
71
+ when '0'..'9', '.', ' ' then key << char
72
+ else raise "DN badly formed"
73
+ end
74
+ when :value then
75
+ case char
76
+ when '\\' then state = :value_normal_escape
77
+ when '"' then state = :value_quoted
78
+ when ' ' then state = :value
79
+ when '#' then
80
+ state = :value_hexstring
81
+ value << char
82
+ when ',' then
83
+ state = :key
84
+ yield key.string.strip, value.string.rstrip
85
+ key = StringIO.new
86
+ value = StringIO.new;
87
+ else
88
+ state = :value_normal
89
+ value << char
90
+ end
91
+ when :value_normal then
92
+ case char
93
+ when '\\' then state = :value_normal_escape
94
+ when ',' then
95
+ state = :key
96
+ yield key.string.strip, value.string.rstrip
97
+ key = StringIO.new
98
+ value = StringIO.new;
99
+ else value << char
100
+ end
101
+ when :value_normal_escape then
102
+ case char
103
+ when '0'..'9', 'a'..'f', 'A'..'F' then
104
+ state = :value_normal_escape_hex
105
+ hex_buffer = char
106
+ else state = :value_normal; value << char
107
+ end
108
+ when :value_normal_escape_hex then
109
+ case char
110
+ when '0'..'9', 'a'..'f', 'A'..'F' then
111
+ state = :value_normal
112
+ value << "#{hex_buffer}#{char}".to_i(16).chr
113
+ else raise "DN badly formed"
114
+ end
115
+ when :value_quoted then
116
+ case char
117
+ when '\\' then state = :value_quoted_escape
118
+ when '"' then state = :value_end
119
+ else value << char
120
+ end
121
+ when :value_quoted_escape then
122
+ case char
123
+ when '0'..'9', 'a'..'f', 'A'..'F' then
124
+ state = :value_quoted_escape_hex
125
+ hex_buffer = char
126
+ else
127
+ state = :value_quoted;
128
+ value << char
129
+ end
130
+ when :value_quoted_escape_hex then
131
+ case char
132
+ when '0'..'9', 'a'..'f', 'A'..'F' then
133
+ state = :value_quoted
134
+ value << "#{hex_buffer}#{char}".to_i(16).chr
135
+ else raise "DN badly formed"
136
+ end
137
+ when :value_hexstring then
138
+ case char
139
+ when '0'..'9', 'a'..'f', 'A'..'F' then
140
+ state = :value_hexstring_hex
141
+ value << char
142
+ when ' ' then state = :value_end
143
+ when ',' then
144
+ state = :key
145
+ yield key.string.strip, value.string.rstrip
146
+ key = StringIO.new
147
+ value = StringIO.new;
148
+ else raise "DN badly formed"
149
+ end
150
+ when :value_hexstring_hex then
151
+ case char
152
+ when '0'..'9', 'a'..'f', 'A'..'F' then
153
+ state = :value_hexstring
154
+ value << char
155
+ else raise "DN badly formed"
156
+ end
157
+ when :value_end then
158
+ case char
159
+ when ' ' then state = :value_end
160
+ when ',' then
161
+ state = :key
162
+ yield key.string.strip, value.string.rstrip
163
+ key = StringIO.new
164
+ value = StringIO.new;
165
+ else raise "DN badly formed"
166
+ end
167
+ else raise "Fell out of state machine"
168
+ end
169
+ end
170
+
171
+ # Last pair
172
+ if [:value, :value_normal, :value_hexstring, :value_end].include? state
173
+ yield key.string.strip, value.string.rstrip
174
+ else
175
+ raise "DN badly formed"
176
+ end
177
+ end
178
+
179
+ ##
180
+ # Returns the DN as an array in the form expected by the constructor.
181
+ def to_a
182
+ a = []
183
+ self.each_pair { |key, value| a << key << value }
184
+ a
185
+ end
186
+
187
+ ##
188
+ # Return the DN as an escaped string.
189
+ def to_s
190
+ @dn
191
+ end
192
+
193
+ # http://tools.ietf.org/html/rfc2253 section 2.4 lists these exceptions
194
+ # for dn values. All of the following must be escaped in any normal string
195
+ # using a single backslash ('\') as escape.
196
+ ESCAPES = {
197
+ ',' => ',',
198
+ '+' => '+',
199
+ '"' => '"',
200
+ '\\' => '\\',
201
+ '<' => '<',
202
+ '>' => '>',
203
+ ';' => ';',
204
+ }
205
+
206
+ # Compiled character class regexp using the keys from the above hash, and
207
+ # checking for a space or # at the start, or space at the end, of the
208
+ # string.
209
+ ESCAPE_RE = Regexp.new("(^ |^#| $|[" +
210
+ ESCAPES.keys.map { |e| Regexp.escape(e) }.join +
211
+ "])")
212
+
213
+ ##
214
+ # Escape a string for use in a DN value
215
+ def self.escape(string)
216
+ string.gsub(ESCAPE_RE) { |char| "\\" + ESCAPES[char] }
217
+ end
218
+
219
+ ##
220
+ # Proxy all other requests to the string object, because a DN is mainly
221
+ # used within the library as a string
222
+ def method_missing(method, *args, &block)
223
+ @dn.send(method, *args, &block)
224
+ end
225
+ end
@@ -0,0 +1,193 @@
1
+ # -*- ruby encoding: utf-8 -*-
2
+ ##
3
+ # Objects of this class represent individual entries in an LDAP directory.
4
+ # User code generally does not instantiate this class. Net::LDAP#search
5
+ # provides objects of this class to user code, either as block parameters or
6
+ # as return values.
7
+ #
8
+ # In LDAP-land, an "entry" is a collection of attributes that are uniquely
9
+ # and globally identified by a DN ("Distinguished Name"). Attributes are
10
+ # identified by short, descriptive words or phrases. Although a directory is
11
+ # free to implement any attribute name, most of them follow rigorous
12
+ # standards so that the range of commonly-encountered attribute names is not
13
+ # large.
14
+ #
15
+ # An attribute name is case-insensitive. Most directories also restrict the
16
+ # range of characters allowed in attribute names. To simplify handling
17
+ # attribute names, Net::LDAP::Entry internally converts them to a standard
18
+ # format. Therefore, the methods which take attribute names can take Strings
19
+ # or Symbols, and work correctly regardless of case or capitalization.
20
+ #
21
+ # An attribute consists of zero or more data items called <i>values.</i> An
22
+ # entry is the combination of a unique DN, a set of attribute names, and a
23
+ # (possibly-empty) array of values for each attribute.
24
+ #
25
+ # Class Net::LDAP::Entry provides convenience methods for dealing with LDAP
26
+ # entries. In addition to the methods documented below, you may access
27
+ # individual attributes of an entry simply by giving the attribute name as
28
+ # the name of a method call. For example:
29
+ #
30
+ # ldap.search( ... ) do |entry|
31
+ # puts "Common name: #{entry.cn}"
32
+ # puts "Email addresses:"
33
+ # entry.mail.each {|ma| puts ma}
34
+ # end
35
+ #
36
+ # If you use this technique to access an attribute that is not present in a
37
+ # particular Entry object, a NoMethodError exception will be raised.
38
+ #
39
+ #--
40
+ # Ugly problem to fix someday: We key off the internal hash with a canonical
41
+ # form of the attribute name: convert to a string, downcase, then take the
42
+ # symbol. Unfortunately we do this in at least three places. Should do it in
43
+ # ONE place.
44
+ class Net::LDAP::Entry
45
+ ##
46
+ # This constructor is not generally called by user code.
47
+ def initialize(dn = nil) #:nodoc:
48
+ @myhash = {}
49
+ @myhash[:dn] = [dn]
50
+ end
51
+
52
+ ##
53
+ # Use the LDIF format for Marshal serialization.
54
+ def _dump(depth) #:nodoc:
55
+ to_ldif
56
+ end
57
+
58
+ ##
59
+ # Use the LDIF format for Marshal serialization.
60
+ def self._load(entry) #:nodoc:
61
+ from_single_ldif_string(entry)
62
+ end
63
+
64
+ class << self
65
+ ##
66
+ # Converts a single LDIF entry string into an Entry object. Useful for
67
+ # Marshal serialization. If a string with multiple LDIF entries is
68
+ # provided, an exception will be raised.
69
+ def from_single_ldif_string(ldif)
70
+ ds = Net::LDAP::Dataset.read_ldif(::StringIO.new(ldif))
71
+
72
+ return nil if ds.empty?
73
+
74
+ raise Net::LDAP::EntryOverflowError, "Too many LDIF entries" unless ds.size == 1
75
+
76
+ entry = ds.to_entries.first
77
+
78
+ return nil if entry.dn.nil?
79
+ entry
80
+ end
81
+
82
+ ##
83
+ # Canonicalizes an LDAP attribute name as a \Symbol. The name is
84
+ # lowercased and, if present, a trailing equals sign is removed.
85
+ def attribute_name(name)
86
+ name = name.to_s.downcase
87
+ name = name[0..-2] if name[-1] == ?=
88
+ name.to_sym
89
+ end
90
+ end
91
+
92
+ ##
93
+ # Sets or replaces the array of values for the provided attribute. The
94
+ # attribute name is canonicalized prior to assignment.
95
+ #
96
+ # When an attribute is set using this, that attribute is now made
97
+ # accessible through methods as well.
98
+ #
99
+ # entry = Net::LDAP::Entry.new("dc=com")
100
+ # entry.foo # => NoMethodError
101
+ # entry["foo"] = 12345 # => [12345]
102
+ # entry.foo # => [12345]
103
+ def []=(name, value)
104
+ @myhash[self.class.attribute_name(name)] = Kernel::Array(value)
105
+ end
106
+
107
+ ##
108
+ # Reads the array of values for the provided attribute. The attribute name
109
+ # is canonicalized prior to reading. Returns an empty array if the
110
+ # attribute does not exist.
111
+ def [](name)
112
+ name = self.class.attribute_name(name)
113
+ @myhash[name] || []
114
+ end
115
+
116
+ ##
117
+ # Read the first value for the provided attribute. The attribute name
118
+ # is canonicalized prior to reading. Returns nil if the attribute does
119
+ # not exist.
120
+ def first(name)
121
+ self[name].first
122
+ end
123
+
124
+ ##
125
+ # Returns the first distinguished name (dn) of the Entry as a \String.
126
+ def dn
127
+ self[:dn].first.to_s
128
+ end
129
+
130
+ ##
131
+ # Returns an array of the attribute names present in the Entry.
132
+ def attribute_names
133
+ @myhash.keys
134
+ end
135
+
136
+ ##
137
+ # Accesses each of the attributes present in the Entry.
138
+ #
139
+ # Calls a user-supplied block with each attribute in turn, passing two
140
+ # arguments to the block: a Symbol giving the name of the attribute, and a
141
+ # (possibly empty) \Array of data values.
142
+ def each # :yields: attribute-name, data-values-array
143
+ if block_given?
144
+ attribute_names.each {|a|
145
+ attr_name,values = a,self[a]
146
+ yield attr_name, values
147
+ }
148
+ end
149
+ end
150
+ alias_method :each_attribute, :each
151
+
152
+ ##
153
+ # Converts the Entry to an LDIF-formatted String
154
+ def to_ldif
155
+ Net::LDAP::Dataset.from_entry(self).to_ldif_string
156
+ end
157
+
158
+ def respond_to?(sym, include_all = false) #:nodoc:
159
+ return true if valid_attribute?(self.class.attribute_name(sym))
160
+ return super
161
+ end
162
+
163
+ def method_missing(sym, *args, &block) #:nodoc:
164
+ name = self.class.attribute_name(sym)
165
+
166
+ if valid_attribute?(name )
167
+ if setter?(sym) && args.size == 1
168
+ value = args.first
169
+ value = Array(value)
170
+ self[name]= value
171
+ return value
172
+ elsif args.empty?
173
+ return self[name]
174
+ end
175
+ end
176
+
177
+ super
178
+ end
179
+
180
+ # Given a valid attribute symbol, returns true.
181
+ def valid_attribute?(attr_name)
182
+ attribute_names.include?(attr_name)
183
+ end
184
+ private :valid_attribute?
185
+
186
+ # Returns true if the symbol ends with an equal sign.
187
+ def setter?(sym)
188
+ sym.to_s[-1] == ?=
189
+ end
190
+ private :setter?
191
+ end # class Entry
192
+
193
+ require 'net/ldap/dataset' unless defined? Net::LDAP::Dataset