rubinius-net-ldap 0.11
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.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rubocop.yml +5 -0
- data/.rubocop_todo.yml +462 -0
- data/.travis.yml +19 -0
- data/CONTRIBUTING.md +54 -0
- data/Contributors.rdoc +24 -0
- data/Gemfile +2 -0
- data/Hacking.rdoc +63 -0
- data/History.rdoc +260 -0
- data/License.rdoc +29 -0
- data/README.rdoc +65 -0
- data/Rakefile +17 -0
- data/lib/net-ldap.rb +2 -0
- data/lib/net/ber.rb +320 -0
- data/lib/net/ber/ber_parser.rb +182 -0
- data/lib/net/ber/core_ext.rb +55 -0
- data/lib/net/ber/core_ext/array.rb +96 -0
- data/lib/net/ber/core_ext/false_class.rb +10 -0
- data/lib/net/ber/core_ext/integer.rb +74 -0
- data/lib/net/ber/core_ext/string.rb +66 -0
- data/lib/net/ber/core_ext/true_class.rb +11 -0
- data/lib/net/ldap.rb +1229 -0
- data/lib/net/ldap/connection.rb +702 -0
- data/lib/net/ldap/dataset.rb +168 -0
- data/lib/net/ldap/dn.rb +225 -0
- data/lib/net/ldap/entry.rb +193 -0
- data/lib/net/ldap/error.rb +38 -0
- data/lib/net/ldap/filter.rb +778 -0
- data/lib/net/ldap/instrumentation.rb +23 -0
- data/lib/net/ldap/password.rb +38 -0
- data/lib/net/ldap/pdu.rb +297 -0
- data/lib/net/ldap/version.rb +5 -0
- data/lib/net/snmp.rb +264 -0
- data/rubinius-net-ldap.gemspec +37 -0
- data/script/install-openldap +112 -0
- data/script/package +7 -0
- data/script/release +16 -0
- data/test/ber/core_ext/test_array.rb +22 -0
- data/test/ber/core_ext/test_string.rb +25 -0
- data/test/ber/test_ber.rb +99 -0
- data/test/fixtures/cacert.pem +20 -0
- data/test/fixtures/openldap/memberof.ldif +33 -0
- data/test/fixtures/openldap/retcode.ldif +76 -0
- data/test/fixtures/openldap/slapd.conf.ldif +67 -0
- data/test/fixtures/seed.ldif +374 -0
- data/test/integration/test_add.rb +28 -0
- data/test/integration/test_ber.rb +30 -0
- data/test/integration/test_bind.rb +34 -0
- data/test/integration/test_delete.rb +31 -0
- data/test/integration/test_open.rb +88 -0
- data/test/integration/test_return_codes.rb +38 -0
- data/test/integration/test_search.rb +77 -0
- data/test/support/vm/openldap/.gitignore +1 -0
- data/test/support/vm/openldap/README.md +32 -0
- data/test/support/vm/openldap/Vagrantfile +33 -0
- data/test/test_dn.rb +44 -0
- data/test/test_entry.rb +65 -0
- data/test/test_filter.rb +223 -0
- data/test/test_filter_parser.rb +20 -0
- data/test/test_helper.rb +66 -0
- data/test/test_ldap.rb +60 -0
- data/test/test_ldap_connection.rb +404 -0
- data/test/test_ldif.rb +104 -0
- data/test/test_password.rb +10 -0
- data/test/test_rename.rb +77 -0
- data/test/test_search.rb +39 -0
- data/test/test_snmp.rb +119 -0
- data/test/test_ssl_ber.rb +40 -0
- data/test/testdata.ldif +101 -0
- data/testserver/ldapserver.rb +210 -0
- data/testserver/testdata.ldif +101 -0
- metadata +204 -0
@@ -0,0 +1,702 @@
|
|
1
|
+
# This is a private class used internally by the library. It should not
|
2
|
+
# be called by user code.
|
3
|
+
class Net::LDAP::Connection #:nodoc:
|
4
|
+
include Net::LDAP::Instrumentation
|
5
|
+
|
6
|
+
LdapVersion = 3
|
7
|
+
MaxSaslChallenges = 10
|
8
|
+
|
9
|
+
def initialize(server)
|
10
|
+
@instrumentation_service = server[:instrumentation_service]
|
11
|
+
|
12
|
+
begin
|
13
|
+
@conn = server[:socket] || TCPSocket.new(server[:host], server[:port])
|
14
|
+
rescue SocketError
|
15
|
+
raise Net::LDAP::Error, "No such address or other socket error."
|
16
|
+
rescue Errno::ECONNREFUSED
|
17
|
+
raise Net::LDAP::Error, "Server #{server[:host]} refused connection on port #{server[:port]}."
|
18
|
+
rescue Errno::EHOSTUNREACH => error
|
19
|
+
raise Net::LDAP::Error, "Host #{server[:host]} was unreachable (#{error.message})"
|
20
|
+
rescue Errno::ETIMEDOUT
|
21
|
+
raise Net::LDAP::Error, "Connection to #{server[:host]} timed out."
|
22
|
+
end
|
23
|
+
|
24
|
+
if server[:encryption]
|
25
|
+
setup_encryption server[:encryption]
|
26
|
+
end
|
27
|
+
|
28
|
+
yield self if block_given?
|
29
|
+
end
|
30
|
+
|
31
|
+
module GetbyteForSSLSocket
|
32
|
+
def getbyte
|
33
|
+
getc.ord
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
module FixSSLSocketSyncClose
|
38
|
+
def close
|
39
|
+
super
|
40
|
+
io.close
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.wrap_with_ssl(io, tls_options = {})
|
45
|
+
raise Net::LDAP::NoOpenSSLError, "OpenSSL is unavailable" unless Net::LDAP::HasOpenSSL
|
46
|
+
|
47
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
48
|
+
|
49
|
+
# By default, we do not verify certificates. For a 1.0 release, this should probably be changed at some point.
|
50
|
+
# See discussion in https://github.com/ruby-ldap/ruby-net-ldap/pull/161
|
51
|
+
ctx.set_params(tls_options) unless tls_options.empty?
|
52
|
+
|
53
|
+
conn = OpenSSL::SSL::SSLSocket.new(io, ctx)
|
54
|
+
conn.connect
|
55
|
+
|
56
|
+
# Doesn't work:
|
57
|
+
# conn.sync_close = true
|
58
|
+
|
59
|
+
conn.extend(GetbyteForSSLSocket) unless conn.respond_to?(:getbyte)
|
60
|
+
conn.extend(FixSSLSocketSyncClose)
|
61
|
+
|
62
|
+
conn
|
63
|
+
end
|
64
|
+
|
65
|
+
#--
|
66
|
+
# Helper method called only from new, and only after we have a
|
67
|
+
# successfully-opened @conn instance variable, which is a TCP connection.
|
68
|
+
# Depending on the received arguments, we establish SSL, potentially
|
69
|
+
# replacing the value of @conn accordingly. Don't generate any errors here
|
70
|
+
# if no encryption is requested. DO raise Net::LDAP::Error objects if encryption
|
71
|
+
# is requested and we have trouble setting it up. That includes if OpenSSL
|
72
|
+
# is not set up on the machine. (Question: how does the Ruby OpenSSL
|
73
|
+
# wrapper react in that case?) DO NOT filter exceptions raised by the
|
74
|
+
# OpenSSL library. Let them pass back to the user. That should make it
|
75
|
+
# easier for us to debug the problem reports. Presumably (hopefully?) that
|
76
|
+
# will also produce recognizable errors if someone tries to use this on a
|
77
|
+
# machine without OpenSSL.
|
78
|
+
#
|
79
|
+
# The simple_tls method is intended as the simplest, stupidest, easiest
|
80
|
+
# solution for people who want nothing more than encrypted comms with the
|
81
|
+
# LDAP server. It doesn't do any server-cert validation and requires
|
82
|
+
# nothing in the way of key files and root-cert files, etc etc. OBSERVE:
|
83
|
+
# WE REPLACE the value of @conn, which is presumed to be a connected
|
84
|
+
# TCPSocket object.
|
85
|
+
#
|
86
|
+
# The start_tls method is supported by many servers over the standard LDAP
|
87
|
+
# port. It does not require an alternative port for encrypted
|
88
|
+
# communications, as with simple_tls. Thanks for Kouhei Sutou for
|
89
|
+
# generously contributing the :start_tls path.
|
90
|
+
#++
|
91
|
+
def setup_encryption(args)
|
92
|
+
args[:tls_options] ||= {}
|
93
|
+
case args[:method]
|
94
|
+
when :simple_tls
|
95
|
+
@conn = self.class.wrap_with_ssl(@conn, args[:tls_options])
|
96
|
+
# additional branches requiring server validation and peer certs, etc.
|
97
|
+
# go here.
|
98
|
+
when :start_tls
|
99
|
+
message_id = next_msgid
|
100
|
+
request = [
|
101
|
+
Net::LDAP::StartTlsOid.to_ber_contextspecific(0)
|
102
|
+
].to_ber_appsequence(Net::LDAP::PDU::ExtendedRequest)
|
103
|
+
|
104
|
+
write(request, nil, message_id)
|
105
|
+
pdu = queued_read(message_id)
|
106
|
+
|
107
|
+
if pdu.nil? || pdu.app_tag != Net::LDAP::PDU::ExtendedResponse
|
108
|
+
raise Net::LDAP::NoStartTLSResultError, "no start_tls result"
|
109
|
+
end
|
110
|
+
|
111
|
+
if pdu.result_code.zero?
|
112
|
+
@conn = self.class.wrap_with_ssl(@conn, args[:tls_options])
|
113
|
+
else
|
114
|
+
raise Net::LDAP::StartTlSError, "start_tls failed: #{pdu.result_code}"
|
115
|
+
end
|
116
|
+
else
|
117
|
+
raise Net::LDAP::EncMethodUnsupportedError, "unsupported encryption method #{args[:method]}"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
#--
|
122
|
+
# This is provided as a convenience method to make sure a connection
|
123
|
+
# object gets closed without waiting for a GC to happen. Clients shouldn't
|
124
|
+
# have to call it, but perhaps it will come in handy someday.
|
125
|
+
#++
|
126
|
+
def close
|
127
|
+
@conn.close
|
128
|
+
@conn = nil
|
129
|
+
end
|
130
|
+
|
131
|
+
# Internal: Reads messages by ID from a queue, falling back to reading from
|
132
|
+
# the connected socket until a message matching the ID is read. Any messages
|
133
|
+
# with mismatched IDs gets queued for subsequent reads by the origin of that
|
134
|
+
# message ID.
|
135
|
+
#
|
136
|
+
# Returns a Net::LDAP::PDU object or nil.
|
137
|
+
def queued_read(message_id)
|
138
|
+
if pdu = message_queue[message_id].shift
|
139
|
+
return pdu
|
140
|
+
end
|
141
|
+
|
142
|
+
# read messages until we have a match for the given message_id
|
143
|
+
while pdu = read
|
144
|
+
if pdu.message_id == message_id
|
145
|
+
return pdu
|
146
|
+
else
|
147
|
+
message_queue[pdu.message_id].push pdu
|
148
|
+
next
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
pdu
|
153
|
+
end
|
154
|
+
|
155
|
+
# Internal: The internal queue of messages, read from the socket, grouped by
|
156
|
+
# message ID.
|
157
|
+
#
|
158
|
+
# Used by `queued_read` to return messages sent by the server with the given
|
159
|
+
# ID. If no messages are queued for that ID, `queued_read` will `read` from
|
160
|
+
# the socket and queue messages that don't match the given ID for other
|
161
|
+
# readers.
|
162
|
+
#
|
163
|
+
# Returns the message queue Hash.
|
164
|
+
def message_queue
|
165
|
+
@message_queue ||= Hash.new do |hash, key|
|
166
|
+
hash[key] = []
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Internal: Reads and parses data from the configured connection.
|
171
|
+
#
|
172
|
+
# - syntax: the BER syntax to use to parse the read data with
|
173
|
+
#
|
174
|
+
# Returns parsed Net::LDAP::PDU object.
|
175
|
+
def read(syntax = Net::LDAP::AsnSyntax)
|
176
|
+
ber_object =
|
177
|
+
instrument "read.net_ldap_connection", :syntax => syntax do |payload|
|
178
|
+
@conn.read_ber(syntax) do |id, content_length|
|
179
|
+
payload[:object_type_id] = id
|
180
|
+
payload[:content_length] = content_length
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
return unless ber_object
|
185
|
+
|
186
|
+
instrument "parse_pdu.net_ldap_connection" do |payload|
|
187
|
+
pdu = payload[:pdu] = Net::LDAP::PDU.new(ber_object)
|
188
|
+
|
189
|
+
payload[:message_id] = pdu.message_id
|
190
|
+
payload[:app_tag] = pdu.app_tag
|
191
|
+
|
192
|
+
pdu
|
193
|
+
end
|
194
|
+
end
|
195
|
+
private :read
|
196
|
+
|
197
|
+
# Internal: Write a BER formatted packet with the next message id to the
|
198
|
+
# configured connection.
|
199
|
+
#
|
200
|
+
# - request: required BER formatted request
|
201
|
+
# - controls: optional BER formatted controls
|
202
|
+
#
|
203
|
+
# Returns the return value from writing to the connection, which in some
|
204
|
+
# cases is the Integer number of bytes written to the socket.
|
205
|
+
def write(request, controls = nil, message_id = next_msgid)
|
206
|
+
instrument "write.net_ldap_connection" do |payload|
|
207
|
+
packet = [message_id.to_ber, request, controls].compact.to_ber_sequence
|
208
|
+
payload[:content_length] = @conn.write(packet)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
private :write
|
212
|
+
|
213
|
+
def next_msgid
|
214
|
+
@msgid ||= 0
|
215
|
+
@msgid += 1
|
216
|
+
end
|
217
|
+
|
218
|
+
def bind(auth)
|
219
|
+
instrument "bind.net_ldap_connection" do |payload|
|
220
|
+
payload[:method] = meth = auth[:method]
|
221
|
+
if [:simple, :anonymous, :anon].include?(meth)
|
222
|
+
bind_simple auth
|
223
|
+
elsif meth == :sasl
|
224
|
+
bind_sasl(auth)
|
225
|
+
elsif meth == :gss_spnego
|
226
|
+
bind_gss_spnego(auth)
|
227
|
+
else
|
228
|
+
raise Net::LDAP::AuthMethodUnsupportedError, "Unsupported auth method (#{meth})"
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
#--
|
234
|
+
# Implements a simple user/psw authentication. Accessed by calling #bind
|
235
|
+
# with a method of :simple or :anonymous.
|
236
|
+
#++
|
237
|
+
def bind_simple(auth)
|
238
|
+
user, psw = if auth[:method] == :simple
|
239
|
+
[auth[:username] || auth[:dn], auth[:password]]
|
240
|
+
else
|
241
|
+
["", ""]
|
242
|
+
end
|
243
|
+
|
244
|
+
raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (user && psw)
|
245
|
+
|
246
|
+
message_id = next_msgid
|
247
|
+
request = [
|
248
|
+
LdapVersion.to_ber, user.to_ber,
|
249
|
+
psw.to_ber_contextspecific(0)
|
250
|
+
].to_ber_appsequence(Net::LDAP::PDU::BindRequest)
|
251
|
+
|
252
|
+
write(request, nil, message_id)
|
253
|
+
pdu = queued_read(message_id)
|
254
|
+
|
255
|
+
if !pdu || pdu.app_tag != Net::LDAP::PDU::BindResult
|
256
|
+
raise Net::LDAP::NoBindResultError, "no bind result"
|
257
|
+
end
|
258
|
+
|
259
|
+
pdu
|
260
|
+
end
|
261
|
+
|
262
|
+
#--
|
263
|
+
# Required parameters: :mechanism, :initial_credential and
|
264
|
+
# :challenge_response
|
265
|
+
#
|
266
|
+
# Mechanism is a string value that will be passed in the SASL-packet's
|
267
|
+
# "mechanism" field.
|
268
|
+
#
|
269
|
+
# Initial credential is most likely a string. It's passed in the initial
|
270
|
+
# BindRequest that goes to the server. In some protocols, it may be empty.
|
271
|
+
#
|
272
|
+
# Challenge-response is a Ruby proc that takes a single parameter and
|
273
|
+
# returns an object that will typically be a string. The
|
274
|
+
# challenge-response block is called when the server returns a
|
275
|
+
# BindResponse with a result code of 14 (saslBindInProgress). The
|
276
|
+
# challenge-response block receives a parameter containing the data
|
277
|
+
# returned by the server in the saslServerCreds field of the LDAP
|
278
|
+
# BindResponse packet. The challenge-response block may be called multiple
|
279
|
+
# times during the course of a SASL authentication, and each time it must
|
280
|
+
# return a value that will be passed back to the server as the credential
|
281
|
+
# data in the next BindRequest packet.
|
282
|
+
#++
|
283
|
+
def bind_sasl(auth)
|
284
|
+
mech, cred, chall = auth[:mechanism], auth[:initial_credential],
|
285
|
+
auth[:challenge_response]
|
286
|
+
raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (mech && cred && chall)
|
287
|
+
|
288
|
+
message_id = next_msgid
|
289
|
+
|
290
|
+
n = 0
|
291
|
+
loop {
|
292
|
+
sasl = [mech.to_ber, cred.to_ber].to_ber_contextspecific(3)
|
293
|
+
request = [
|
294
|
+
LdapVersion.to_ber, "".to_ber, sasl
|
295
|
+
].to_ber_appsequence(Net::LDAP::PDU::BindRequest)
|
296
|
+
|
297
|
+
write(request, nil, message_id)
|
298
|
+
pdu = queued_read(message_id)
|
299
|
+
|
300
|
+
if !pdu || pdu.app_tag != Net::LDAP::PDU::BindResult
|
301
|
+
raise Net::LDAP::NoBindResultError, "no bind result"
|
302
|
+
end
|
303
|
+
|
304
|
+
return pdu unless pdu.result_code == Net::LDAP::ResultCodeSaslBindInProgress
|
305
|
+
raise Net::LDAP::SASLChallengeOverflowError, "sasl-challenge overflow" if ((n += 1) > MaxSaslChallenges)
|
306
|
+
|
307
|
+
cred = chall.call(pdu.result_server_sasl_creds)
|
308
|
+
}
|
309
|
+
|
310
|
+
raise Net::LDAP::SASLChallengeOverflowError, "why are we here?"
|
311
|
+
end
|
312
|
+
private :bind_sasl
|
313
|
+
|
314
|
+
#--
|
315
|
+
# PROVISIONAL, only for testing SASL implementations. DON'T USE THIS YET.
|
316
|
+
# Uses Kohei Kajimoto's Ruby/NTLM. We have to find a clean way to
|
317
|
+
# integrate it without introducing an external dependency.
|
318
|
+
#
|
319
|
+
# This authentication method is accessed by calling #bind with a :method
|
320
|
+
# parameter of :gss_spnego. It requires :username and :password
|
321
|
+
# attributes, just like the :simple authentication method. It performs a
|
322
|
+
# GSS-SPNEGO authentication with the server, which is presumed to be a
|
323
|
+
# Microsoft Active Directory.
|
324
|
+
#++
|
325
|
+
def bind_gss_spnego(auth)
|
326
|
+
require 'ntlm'
|
327
|
+
|
328
|
+
user, psw = [auth[:username] || auth[:dn], auth[:password]]
|
329
|
+
raise Net::LDAP::BindingInformationInvalidError, "Invalid binding information" unless (user && psw)
|
330
|
+
|
331
|
+
nego = proc { |challenge|
|
332
|
+
t2_msg = NTLM::Message.parse(challenge)
|
333
|
+
t3_msg = t2_msg.response({ :user => user, :password => psw },
|
334
|
+
{ :ntlmv2 => true })
|
335
|
+
t3_msg.serialize
|
336
|
+
}
|
337
|
+
|
338
|
+
bind_sasl(:method => :sasl, :mechanism => "GSS-SPNEGO",
|
339
|
+
:initial_credential => NTLM::Message::Type1.new.serialize,
|
340
|
+
:challenge_response => nego)
|
341
|
+
end
|
342
|
+
private :bind_gss_spnego
|
343
|
+
|
344
|
+
|
345
|
+
#--
|
346
|
+
# Allow the caller to specify a sort control
|
347
|
+
#
|
348
|
+
# The format of the sort control needs to be:
|
349
|
+
#
|
350
|
+
# :sort_control => ["cn"] # just a string
|
351
|
+
# or
|
352
|
+
# :sort_control => [["cn", "matchingRule", true]] #attribute, matchingRule, direction (true / false)
|
353
|
+
# or
|
354
|
+
# :sort_control => ["givenname","sn"] #multiple strings or arrays
|
355
|
+
#
|
356
|
+
def encode_sort_controls(sort_definitions)
|
357
|
+
return sort_definitions unless sort_definitions
|
358
|
+
|
359
|
+
sort_control_values = sort_definitions.map do |control|
|
360
|
+
control = Array(control) # if there is only an attribute name as a string then infer the orderinrule and reverseorder
|
361
|
+
control[0] = String(control[0]).to_ber,
|
362
|
+
control[1] = String(control[1]).to_ber,
|
363
|
+
control[2] = (control[2] == true).to_ber
|
364
|
+
control.to_ber_sequence
|
365
|
+
end
|
366
|
+
sort_control = [
|
367
|
+
Net::LDAP::LDAPControls::SORT_REQUEST.to_ber,
|
368
|
+
false.to_ber,
|
369
|
+
sort_control_values.to_ber_sequence.to_s.to_ber
|
370
|
+
].to_ber_sequence
|
371
|
+
end
|
372
|
+
|
373
|
+
#--
|
374
|
+
# Alternate implementation, this yields each search entry to the caller as
|
375
|
+
# it are received.
|
376
|
+
#
|
377
|
+
# TODO: certain search parameters are hardcoded.
|
378
|
+
# TODO: if we mis-parse the server results or the results are wrong, we
|
379
|
+
# can block forever. That's because we keep reading results until we get a
|
380
|
+
# type-5 packet, which might never come. We need to support the time-limit
|
381
|
+
# in the protocol.
|
382
|
+
#++
|
383
|
+
def search(args = nil)
|
384
|
+
args ||= {}
|
385
|
+
|
386
|
+
# filtering, scoping, search base
|
387
|
+
# filter: https://tools.ietf.org/html/rfc4511#section-4.5.1.7
|
388
|
+
# base: https://tools.ietf.org/html/rfc4511#section-4.5.1.1
|
389
|
+
# scope: https://tools.ietf.org/html/rfc4511#section-4.5.1.2
|
390
|
+
filter = args[:filter] || Net::LDAP::Filter.eq("objectClass", "*")
|
391
|
+
base = args[:base]
|
392
|
+
scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree
|
393
|
+
|
394
|
+
# attr handling
|
395
|
+
# attrs: https://tools.ietf.org/html/rfc4511#section-4.5.1.8
|
396
|
+
# attrs_only: https://tools.ietf.org/html/rfc4511#section-4.5.1.6
|
397
|
+
attrs = Array(args[:attributes])
|
398
|
+
attrs_only = args[:attributes_only] == true
|
399
|
+
|
400
|
+
# references
|
401
|
+
# refs: https://tools.ietf.org/html/rfc4511#section-4.5.3
|
402
|
+
# deref: https://tools.ietf.org/html/rfc4511#section-4.5.1.3
|
403
|
+
refs = args[:return_referrals] == true
|
404
|
+
deref = args[:deref] || Net::LDAP::DerefAliases_Never
|
405
|
+
|
406
|
+
# limiting, paging, sorting
|
407
|
+
# size: https://tools.ietf.org/html/rfc4511#section-4.5.1.4
|
408
|
+
# time: https://tools.ietf.org/html/rfc4511#section-4.5.1.5
|
409
|
+
size = args[:size].to_i
|
410
|
+
time = args[:time].to_i
|
411
|
+
paged = args[:paged_searches_supported]
|
412
|
+
sort = args.fetch(:sort_controls, false)
|
413
|
+
|
414
|
+
# arg validation
|
415
|
+
raise ArgumentError, "search base is required" unless base
|
416
|
+
raise ArgumentError, "invalid search-size" unless size >= 0
|
417
|
+
raise ArgumentError, "invalid search scope" unless Net::LDAP::SearchScopes.include?(scope)
|
418
|
+
raise ArgumentError, "invalid alias dereferencing value" unless Net::LDAP::DerefAliasesArray.include?(deref)
|
419
|
+
|
420
|
+
# arg transforms
|
421
|
+
filter = Net::LDAP::Filter.construct(filter) if filter.is_a?(String)
|
422
|
+
ber_attrs = attrs.map { |attr| attr.to_s.to_ber }
|
423
|
+
ber_sort = encode_sort_controls(sort)
|
424
|
+
|
425
|
+
# An interesting value for the size limit would be close to A/D's
|
426
|
+
# built-in page limit of 1000 records, but openLDAP newer than version
|
427
|
+
# 2.2.0 chokes on anything bigger than 126. You get a silent error that
|
428
|
+
# is easily visible by running slapd in debug mode. Go figure.
|
429
|
+
#
|
430
|
+
# Changed this around 06Sep06 to support a caller-specified search-size
|
431
|
+
# limit. Because we ALWAYS do paged searches, we have to work around the
|
432
|
+
# problem that it's not legal to specify a "normal" sizelimit (in the
|
433
|
+
# body of the search request) that is larger than the page size we're
|
434
|
+
# requesting. Unfortunately, I have the feeling that this will break
|
435
|
+
# with LDAP servers that don't support paged searches!!!
|
436
|
+
#
|
437
|
+
# (Because we pass zero as the sizelimit on search rounds when the
|
438
|
+
# remaining limit is larger than our max page size of 126. In these
|
439
|
+
# cases, I think the caller's search limit will be ignored!)
|
440
|
+
#
|
441
|
+
# CONFIRMED: This code doesn't work on LDAPs that don't support paged
|
442
|
+
# searches when the size limit is larger than 126. We're going to have
|
443
|
+
# to do a root-DSE record search and not do a paged search if the LDAP
|
444
|
+
# doesn't support it. Yuck.
|
445
|
+
rfc2696_cookie = [126, ""]
|
446
|
+
result_pdu = nil
|
447
|
+
n_results = 0
|
448
|
+
|
449
|
+
message_id = next_msgid
|
450
|
+
|
451
|
+
instrument "search.net_ldap_connection",
|
452
|
+
:message_id => message_id,
|
453
|
+
:filter => filter,
|
454
|
+
:base => base,
|
455
|
+
:scope => scope,
|
456
|
+
:size => size,
|
457
|
+
:time => time,
|
458
|
+
:sort => sort,
|
459
|
+
:referrals => refs,
|
460
|
+
:deref => deref,
|
461
|
+
:attributes => attrs do |payload|
|
462
|
+
loop do
|
463
|
+
# should collect this into a private helper to clarify the structure
|
464
|
+
query_limit = 0
|
465
|
+
if size > 0
|
466
|
+
if paged
|
467
|
+
query_limit = (((size - n_results) < 126) ? (size -
|
468
|
+
n_results) : 0)
|
469
|
+
else
|
470
|
+
query_limit = size
|
471
|
+
end
|
472
|
+
end
|
473
|
+
|
474
|
+
request = [
|
475
|
+
base.to_ber,
|
476
|
+
scope.to_ber_enumerated,
|
477
|
+
deref.to_ber_enumerated,
|
478
|
+
query_limit.to_ber, # size limit
|
479
|
+
time.to_ber,
|
480
|
+
attrs_only.to_ber,
|
481
|
+
filter.to_ber,
|
482
|
+
ber_attrs.to_ber_sequence
|
483
|
+
].to_ber_appsequence(Net::LDAP::PDU::SearchRequest)
|
484
|
+
|
485
|
+
# rfc2696_cookie sometimes contains binary data from Microsoft Active Directory
|
486
|
+
# this breaks when calling to_ber. (Can't force binary data to UTF-8)
|
487
|
+
# we have to disable paging (even though server supports it) to get around this...
|
488
|
+
|
489
|
+
controls = []
|
490
|
+
controls <<
|
491
|
+
[
|
492
|
+
Net::LDAP::LDAPControls::PAGED_RESULTS.to_ber,
|
493
|
+
# Criticality MUST be false to interoperate with normal LDAPs.
|
494
|
+
false.to_ber,
|
495
|
+
rfc2696_cookie.map{ |v| v.to_ber}.to_ber_sequence.to_s.to_ber
|
496
|
+
].to_ber_sequence if paged
|
497
|
+
controls << ber_sort if ber_sort
|
498
|
+
controls = controls.empty? ? nil : controls.to_ber_contextspecific(0)
|
499
|
+
|
500
|
+
write(request, controls, message_id)
|
501
|
+
|
502
|
+
result_pdu = nil
|
503
|
+
controls = []
|
504
|
+
|
505
|
+
while pdu = queued_read(message_id)
|
506
|
+
case pdu.app_tag
|
507
|
+
when Net::LDAP::PDU::SearchReturnedData
|
508
|
+
n_results += 1
|
509
|
+
yield pdu.search_entry if block_given?
|
510
|
+
when Net::LDAP::PDU::SearchResultReferral
|
511
|
+
if refs
|
512
|
+
if block_given?
|
513
|
+
se = Net::LDAP::Entry.new
|
514
|
+
se[:search_referrals] = (pdu.search_referrals || [])
|
515
|
+
yield se
|
516
|
+
end
|
517
|
+
end
|
518
|
+
when Net::LDAP::PDU::SearchResult
|
519
|
+
result_pdu = pdu
|
520
|
+
controls = pdu.result_controls
|
521
|
+
if refs && pdu.result_code == Net::LDAP::ResultCodeReferral
|
522
|
+
if block_given?
|
523
|
+
se = Net::LDAP::Entry.new
|
524
|
+
se[:search_referrals] = (pdu.search_referrals || [])
|
525
|
+
yield se
|
526
|
+
end
|
527
|
+
end
|
528
|
+
break
|
529
|
+
else
|
530
|
+
raise Net::LDAP::ResponseTypeInvalidError, "invalid response-type in search: #{pdu.app_tag}"
|
531
|
+
end
|
532
|
+
end
|
533
|
+
|
534
|
+
# count number of pages of results
|
535
|
+
payload[:page_count] ||= 0
|
536
|
+
payload[:page_count] += 1
|
537
|
+
|
538
|
+
# When we get here, we have seen a type-5 response. If there is no
|
539
|
+
# error AND there is an RFC-2696 cookie, then query again for the next
|
540
|
+
# page of results. If not, we're done. Don't screw this up or we'll
|
541
|
+
# break every search we do.
|
542
|
+
#
|
543
|
+
# Noticed 02Sep06, look at the read_ber call in this loop, shouldn't
|
544
|
+
# that have a parameter of AsnSyntax? Does this just accidentally
|
545
|
+
# work? According to RFC-2696, the value expected in this position is
|
546
|
+
# of type OCTET STRING, covered in the default syntax supported by
|
547
|
+
# read_ber, so I guess we're ok.
|
548
|
+
more_pages = false
|
549
|
+
if result_pdu.result_code == Net::LDAP::ResultCodeSuccess and controls
|
550
|
+
controls.each do |c|
|
551
|
+
if c.oid == Net::LDAP::LDAPControls::PAGED_RESULTS
|
552
|
+
# just in case some bogus server sends us more than 1 of these.
|
553
|
+
more_pages = false
|
554
|
+
if c.value and c.value.length > 0
|
555
|
+
cookie = c.value.read_ber[1]
|
556
|
+
if cookie and cookie.length > 0
|
557
|
+
rfc2696_cookie[1] = cookie
|
558
|
+
more_pages = true
|
559
|
+
end
|
560
|
+
end
|
561
|
+
end
|
562
|
+
end
|
563
|
+
end
|
564
|
+
|
565
|
+
break unless more_pages
|
566
|
+
end # loop
|
567
|
+
|
568
|
+
# track total result count
|
569
|
+
payload[:result_count] = n_results
|
570
|
+
|
571
|
+
result_pdu || OpenStruct.new(:status => :failure, :result_code => Net::LDAP::ResultCodeOperationsError, :message => "Invalid search")
|
572
|
+
end # instrument
|
573
|
+
ensure
|
574
|
+
|
575
|
+
# clean up message queue for this search
|
576
|
+
messages = message_queue.delete(message_id)
|
577
|
+
|
578
|
+
# in the exceptional case some messages were *not* consumed from the queue,
|
579
|
+
# instrument the event but do not fail.
|
580
|
+
if !messages.nil? && !messages.empty?
|
581
|
+
instrument "search_messages_unread.net_ldap_connection",
|
582
|
+
message_id => message_id, messages => messages
|
583
|
+
end
|
584
|
+
end
|
585
|
+
|
586
|
+
MODIFY_OPERATIONS = { #:nodoc:
|
587
|
+
:add => 0,
|
588
|
+
:delete => 1,
|
589
|
+
:replace => 2
|
590
|
+
}
|
591
|
+
|
592
|
+
def self.modify_ops(operations)
|
593
|
+
ops = []
|
594
|
+
if operations
|
595
|
+
operations.each { |op, attrib, values|
|
596
|
+
# TODO, fix the following line, which gives a bogus error if the
|
597
|
+
# opcode is invalid.
|
598
|
+
op_ber = MODIFY_OPERATIONS[op.to_sym].to_ber_enumerated
|
599
|
+
values = [ values ].flatten.map { |v| v.to_ber if v }.to_ber_set
|
600
|
+
values = [ attrib.to_s.to_ber, values ].to_ber_sequence
|
601
|
+
ops << [ op_ber, values ].to_ber
|
602
|
+
}
|
603
|
+
end
|
604
|
+
ops
|
605
|
+
end
|
606
|
+
|
607
|
+
#--
|
608
|
+
# TODO: need to support a time limit, in case the server fails to respond.
|
609
|
+
# TODO: We're throwing an exception here on empty DN. Should return a
|
610
|
+
# proper error instead, probaby from farther up the chain.
|
611
|
+
# TODO: If the user specifies a bogus opcode, we'll throw a confusing
|
612
|
+
# error here ("to_ber_enumerated is not defined on nil").
|
613
|
+
#++
|
614
|
+
def modify(args)
|
615
|
+
modify_dn = args[:dn] or raise "Unable to modify empty DN"
|
616
|
+
ops = self.class.modify_ops args[:operations]
|
617
|
+
|
618
|
+
message_id = next_msgid
|
619
|
+
request = [
|
620
|
+
modify_dn.to_ber,
|
621
|
+
ops.to_ber_sequence
|
622
|
+
].to_ber_appsequence(Net::LDAP::PDU::ModifyRequest)
|
623
|
+
|
624
|
+
write(request, nil, message_id)
|
625
|
+
pdu = queued_read(message_id)
|
626
|
+
|
627
|
+
if !pdu || pdu.app_tag != Net::LDAP::PDU::ModifyResponse
|
628
|
+
raise Net::LDAP::ResponseMissingOrInvalidError, "response missing or invalid"
|
629
|
+
end
|
630
|
+
|
631
|
+
pdu
|
632
|
+
end
|
633
|
+
|
634
|
+
#--
|
635
|
+
# TODO: need to support a time limit, in case the server fails to respond.
|
636
|
+
# Unlike other operation-methods in this class, we return a result hash
|
637
|
+
# rather than a simple result number. This is experimental, and eventually
|
638
|
+
# we'll want to do this with all the others. The point is to have access
|
639
|
+
# to the error message and the matched-DN returned by the server.
|
640
|
+
#++
|
641
|
+
def add(args)
|
642
|
+
add_dn = args[:dn] or raise Net::LDAP::EmptyDNError, "Unable to add empty DN"
|
643
|
+
add_attrs = []
|
644
|
+
a = args[:attributes] and a.each { |k, v|
|
645
|
+
add_attrs << [ k.to_s.to_ber, Array(v).map { |m| m.to_ber}.to_ber_set ].to_ber_sequence
|
646
|
+
}
|
647
|
+
|
648
|
+
message_id = next_msgid
|
649
|
+
request = [add_dn.to_ber, add_attrs.to_ber_sequence].to_ber_appsequence(Net::LDAP::PDU::AddRequest)
|
650
|
+
|
651
|
+
write(request, nil, message_id)
|
652
|
+
pdu = queued_read(message_id)
|
653
|
+
|
654
|
+
if !pdu || pdu.app_tag != Net::LDAP::PDU::AddResponse
|
655
|
+
raise Net::LDAP::ResponseMissingError, "response missing or invalid"
|
656
|
+
end
|
657
|
+
|
658
|
+
pdu
|
659
|
+
end
|
660
|
+
|
661
|
+
#--
|
662
|
+
# TODO: need to support a time limit, in case the server fails to respond.
|
663
|
+
#++
|
664
|
+
def rename(args)
|
665
|
+
old_dn = args[:olddn] or raise "Unable to rename empty DN"
|
666
|
+
new_rdn = args[:newrdn] or raise "Unable to rename to empty RDN"
|
667
|
+
delete_attrs = args[:delete_attributes] ? true : false
|
668
|
+
new_superior = args[:new_superior]
|
669
|
+
|
670
|
+
message_id = next_msgid
|
671
|
+
request = [old_dn.to_ber, new_rdn.to_ber, delete_attrs.to_ber]
|
672
|
+
request << new_superior.to_ber_contextspecific(0) unless new_superior == nil
|
673
|
+
|
674
|
+
write(request.to_ber_appsequence(Net::LDAP::PDU::ModifyRDNRequest), nil, message_id)
|
675
|
+
pdu = queued_read(message_id)
|
676
|
+
|
677
|
+
if !pdu || pdu.app_tag != Net::LDAP::PDU::ModifyRDNResponse
|
678
|
+
raise Net::LDAP::ResponseMissingOrInvalidError.new "response missing or invalid"
|
679
|
+
end
|
680
|
+
|
681
|
+
pdu
|
682
|
+
end
|
683
|
+
|
684
|
+
#--
|
685
|
+
# TODO, need to support a time limit, in case the server fails to respond.
|
686
|
+
#++
|
687
|
+
def delete(args)
|
688
|
+
dn = args[:dn] or raise "Unable to delete empty DN"
|
689
|
+
controls = args.include?(:control_codes) ? args[:control_codes].to_ber_control : nil #use nil so we can compact later
|
690
|
+
message_id = next_msgid
|
691
|
+
request = dn.to_s.to_ber_application_string(Net::LDAP::PDU::DeleteRequest)
|
692
|
+
|
693
|
+
write(request, controls, message_id)
|
694
|
+
pdu = queued_read(message_id)
|
695
|
+
|
696
|
+
if !pdu || pdu.app_tag != Net::LDAP::PDU::DeleteResponse
|
697
|
+
raise Net::LDAP::ResponseMissingOrInvalidError, "response missing or invalid"
|
698
|
+
end
|
699
|
+
|
700
|
+
pdu
|
701
|
+
end
|
702
|
+
end # class Connection
|