Skip to content

Commit 9febcbe

Browse files
author
Jerry Cheung
committed
Merge remote-tracking branch 'origin/master' into search-integration-tests
Conflicts: test/integration/test_search.rb
2 parents a47e357 + f40c46e commit 9febcbe

File tree

8 files changed

+238
-52
lines changed

8 files changed

+238
-52
lines changed

lib/net/ldap.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,7 @@ def open
610610
# Net::LDAP::SearchScope_WholeSubtree. Default is WholeSubtree.)
611611
# * :size (an integer indicating the maximum number of search entries to
612612
# return. Default is zero, which signifies no limit.)
613+
# * :time (an integer restricting the maximum time in seconds allowed for a search. Default is zero, no time limit RFC 4511 4.5.1.5)
613614
# * :deref (one of: Net::LDAP::DerefAliases_Never, Net::LDAP::DerefAliases_Search,
614615
# Net::LDAP::DerefAliases_Find, Net::LDAP::DerefAliases_Always. Default is Never.)
615616
#
@@ -678,7 +679,18 @@ def search(args = {})
678679
end
679680

680681
if return_result_set
681-
(!@result.nil? && @result.result_code == 0) ? result_set : nil
682+
unless @result.nil?
683+
case @result.result_code
684+
when ResultStrings.key("Success")
685+
# everything good
686+
result_set
687+
when ResultStrings.key("Size Limit Exceeded"), ResultStrings.key("Time Limit Exceeded")
688+
# LDAP: Size/Time limit exceeded
689+
# This happens when we use size option and results are truncated
690+
# Still we need to return user results
691+
result_set
692+
end
693+
end
682694
else
683695
@result.success?
684696
end

lib/net/ldap/connection.rb

Lines changed: 117 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,45 @@ def close
111111
@conn = nil
112112
end
113113

114+
# Internal: Reads messages by ID from a queue, falling back to reading from
115+
# the connected socket until a message matching the ID is read. Any messages
116+
# with mismatched IDs gets queued for subsequent reads by the origin of that
117+
# message ID.
118+
#
119+
# Returns a Net::LDAP::PDU object or nil.
120+
def queued_read(message_id)
121+
if pdu = message_queue[message_id].shift
122+
return pdu
123+
end
124+
125+
# read messages until we have a match for the given message_id
126+
while pdu = read
127+
if pdu.message_id == message_id
128+
return pdu
129+
else
130+
message_queue[pdu.message_id].push pdu
131+
next
132+
end
133+
end
134+
135+
pdu
136+
end
137+
138+
# Internal: The internal queue of messages, read from the socket, grouped by
139+
# message ID.
140+
#
141+
# Used by `queued_read` to return messages sent by the server with the given
142+
# ID. If no messages are queued for that ID, `queued_read` will `read` from
143+
# the socket and queue messages that don't match the given ID for other
144+
# readers.
145+
#
146+
# Returns the message queue Hash.
147+
def message_queue
148+
@message_queue ||= Hash.new do |hash, key|
149+
hash[key] = []
150+
end
151+
end
152+
114153
# Internal: Reads and parses data from the configured connection.
115154
#
116155
# - syntax: the BER syntax to use to parse the read data with
@@ -146,9 +185,9 @@ def read(syntax = Net::LDAP::AsnSyntax)
146185
#
147186
# Returns the return value from writing to the connection, which in some
148187
# cases is the Integer number of bytes written to the socket.
149-
def write(request, controls = nil)
188+
def write(request, controls = nil, message_id = next_msgid)
150189
instrument "write.net_ldap_connection" do |payload|
151-
packet = [next_msgid.to_ber, request, controls].compact.to_ber_sequence
190+
packet = [message_id.to_ber, request, controls].compact.to_ber_sequence
152191
payload[:content_length] = @conn.write(packet)
153192
end
154193
end
@@ -311,26 +350,47 @@ def encode_sort_controls(sort_definitions)
311350
# type-5 packet, which might never come. We need to support the time-limit
312351
# in the protocol.
313352
#++
314-
def search(args = {})
315-
search_filter = (args && args[:filter]) ||
316-
Net::LDAP::Filter.eq("objectclass", "*")
317-
search_filter = Net::LDAP::Filter.construct(search_filter) if search_filter.is_a?(String)
318-
search_base = (args && args[:base]) || "dc=example, dc=com"
319-
search_attributes = ((args && args[:attributes]) || []).map { |attr| attr.to_s.to_ber}
320-
return_referrals = args && args[:return_referrals] == true
321-
sizelimit = (args && args[:size].to_i) || 0
322-
raise Net::LDAP::LdapError, "invalid search-size" unless sizelimit >= 0
323-
paged_searches_supported = (args && args[:paged_searches_supported])
324-
325-
attributes_only = (args and args[:attributes_only] == true)
326-
scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree
353+
def search(args = nil)
354+
args ||= {}
355+
356+
# filtering, scoping, search base
357+
# filter: https://tools.ietf.org/html/rfc4511#section-4.5.1.7
358+
# base: https://tools.ietf.org/html/rfc4511#section-4.5.1.1
359+
# scope: https://tools.ietf.org/html/rfc4511#section-4.5.1.2
360+
filter = args[:filter] || Net::LDAP::Filter.eq("objectClass", "*")
361+
base = args[:base]
362+
scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree
363+
364+
# attr handling
365+
# attrs: https://tools.ietf.org/html/rfc4511#section-4.5.1.8
366+
# attrs_only: https://tools.ietf.org/html/rfc4511#section-4.5.1.6
367+
attrs = Array(args[:attributes])
368+
attrs_only = args[:attributes_only] == true
369+
370+
# references
371+
# refs: https://tools.ietf.org/html/rfc4511#section-4.5.3
372+
# deref: https://tools.ietf.org/html/rfc4511#section-4.5.1.3
373+
refs = args[:return_referrals] == true
374+
deref = args[:deref] || Net::LDAP::DerefAliases_Never
375+
376+
# limiting, paging, sorting
377+
# size: https://tools.ietf.org/html/rfc4511#section-4.5.1.4
378+
# time: https://tools.ietf.org/html/rfc4511#section-4.5.1.5
379+
size = args[:size].to_i
380+
time = args[:time].to_i
381+
paged = args[:paged_searches_supported]
382+
sort = args.fetch(:sort_controls, false)
383+
384+
# arg validation
385+
raise Net::LDAP::LdapError, "search base is required" unless base
386+
raise Net::LDAP::LdapError, "invalid search-size" unless size >= 0
327387
raise Net::LDAP::LdapError, "invalid search scope" unless Net::LDAP::SearchScopes.include?(scope)
388+
raise Net::LDAP::LdapError, "invalid alias dereferencing value" unless Net::LDAP::DerefAliasesArray.include?(deref)
328389

329-
sort_control = encode_sort_controls(args.fetch(:sort_controls){ false })
330-
331-
deref = args[:deref] || Net::LDAP::DerefAliases_Never
332-
raise Net::LDAP::LdapError.new( "invalid alias dereferencing value" ) unless Net::LDAP::DerefAliasesArray.include?(deref)
333-
390+
# arg transforms
391+
filter = Net::LDAP::Filter.construct(filter) if filter.is_a?(String)
392+
ber_attrs = attrs.map { |attr| attr.to_s.to_ber }
393+
ber_sort = encode_sort_controls(sort)
334394

335395
# An interesting value for the size limit would be close to A/D's
336396
# built-in page limit of 1000 records, but openLDAP newer than version
@@ -356,36 +416,40 @@ def search(args = {})
356416
result_pdu = nil
357417
n_results = 0
358418

419+
message_id = next_msgid
420+
359421
instrument "search.net_ldap_connection",
360-
:filter => search_filter,
361-
:base => search_base,
362-
:scope => scope,
363-
:limit => sizelimit,
364-
:sort => sort_control,
365-
:referrals => return_referrals,
366-
:deref => deref,
367-
:attributes => search_attributes do |payload|
422+
message_id: message_id,
423+
filter: filter,
424+
base: base,
425+
scope: scope,
426+
size: size,
427+
time: time,
428+
sort: sort,
429+
referrals: refs,
430+
deref: deref,
431+
attributes: attrs do |payload|
368432
loop do
369433
# should collect this into a private helper to clarify the structure
370434
query_limit = 0
371-
if sizelimit > 0
372-
if paged_searches_supported
373-
query_limit = (((sizelimit - n_results) < 126) ? (sizelimit -
435+
if size > 0
436+
if paged
437+
query_limit = (((size - n_results) < 126) ? (size -
374438
n_results) : 0)
375439
else
376-
query_limit = sizelimit
440+
query_limit = size
377441
end
378442
end
379443

380444
request = [
381-
search_base.to_ber,
445+
base.to_ber,
382446
scope.to_ber_enumerated,
383447
deref.to_ber_enumerated,
384448
query_limit.to_ber, # size limit
385-
0.to_ber,
386-
attributes_only.to_ber,
387-
search_filter.to_ber,
388-
search_attributes.to_ber_sequence
449+
time.to_ber,
450+
attrs_only.to_ber,
451+
filter.to_ber,
452+
ber_attrs.to_ber_sequence
389453
].to_ber_appsequence(3)
390454

391455
# rfc2696_cookie sometimes contains binary data from Microsoft Active Directory
@@ -399,22 +463,22 @@ def search(args = {})
399463
# Criticality MUST be false to interoperate with normal LDAPs.
400464
false.to_ber,
401465
rfc2696_cookie.map{ |v| v.to_ber}.to_ber_sequence.to_s.to_ber
402-
].to_ber_sequence if paged_searches_supported
403-
controls << sort_control if sort_control
466+
].to_ber_sequence if paged
467+
controls << ber_sort if ber_sort
404468
controls = controls.empty? ? nil : controls.to_ber_contextspecific(0)
405469

406-
write(request, controls)
470+
write(request, controls, message_id)
407471

408472
result_pdu = nil
409473
controls = []
410474

411-
while pdu = read
475+
while pdu = queued_read(message_id)
412476
case pdu.app_tag
413477
when Net::LDAP::PDU::SearchReturnedData
414478
n_results += 1
415479
yield pdu.search_entry if block_given?
416480
when Net::LDAP::PDU::SearchResultReferral
417-
if return_referrals
481+
if refs
418482
if block_given?
419483
se = Net::LDAP::Entry.new
420484
se[:search_referrals] = (pdu.search_referrals || [])
@@ -424,7 +488,7 @@ def search(args = {})
424488
when Net::LDAP::PDU::SearchResult
425489
result_pdu = pdu
426490
controls = pdu.result_controls
427-
if return_referrals && pdu.result_code == 10
491+
if refs && pdu.result_code == 10
428492
if block_given?
429493
se = Net::LDAP::Entry.new
430494
se[:search_referrals] = (pdu.search_referrals || [])
@@ -476,6 +540,16 @@ def search(args = {})
476540

477541
result_pdu || OpenStruct.new(:status => :failure, :result_code => 1, :message => "Invalid search")
478542
end # instrument
543+
ensure
544+
# clean up message queue for this search
545+
messages = message_queue.delete(message_id)
546+
547+
# in the exceptional case some messages were *not* consumed from the queue,
548+
# instrument the event but do not fail.
549+
unless messages.empty?
550+
instrument "search_messages_unread.net_ldap_connection",
551+
message_id: message_id, messages: messages
552+
end
479553
end
480554

481555
MODIFY_OPERATIONS = { #:nodoc:

test/integration/test_delete.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ def setup
1414
sn: "delete-user1",
1515
1616
}
17-
assert @ldap.add(dn: @dn, attributes: attrs), @ldap.get_operation_result.inspect
17+
unless @ldap.search(base: @dn, scope: Net::LDAP::SearchScope_BaseObject)
18+
assert @ldap.add(dn: @dn, attributes: attrs), @ldap.get_operation_result.inspect
19+
end
1820
assert @ldap.search(base: @dn, scope: Net::LDAP::SearchScope_BaseObject)
1921
end
2022

test/integration/test_open.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
require_relative '../test_helper'
2+
3+
class TestBindIntegration < LDAPIntegrationTestCase
4+
def test_binds_without_open
5+
events = @service.subscribe "bind.net_ldap_connection"
6+
7+
@ldap.search(filter: "uid=user1", base: "ou=People,dc=rubyldap,dc=com", ignore_server_caps: true)
8+
@ldap.search(filter: "uid=user1", base: "ou=People,dc=rubyldap,dc=com", ignore_server_caps: true)
9+
10+
assert_equal 2, events.size
11+
end
12+
13+
def test_binds_with_open
14+
events = @service.subscribe "bind.net_ldap_connection"
15+
16+
@ldap.open do
17+
@ldap.search(filter: "uid=user1", base: "ou=People,dc=rubyldap,dc=com", ignore_server_caps: true)
18+
@ldap.search(filter: "uid=user1", base: "ou=People,dc=rubyldap,dc=com", ignore_server_caps: true)
19+
end
20+
21+
assert_equal 1, events.size
22+
end
23+
24+
def test_nested_search_without_open
25+
entries = []
26+
nested_entry = nil
27+
28+
@ldap.search(filter: "(|(uid=user1)(uid=user2))", base: "ou=People,dc=rubyldap,dc=com") do |entry|
29+
entries << entry.uid.first
30+
nested_entry ||= @ldap.search(filter: "uid=user3", base: "ou=People,dc=rubyldap,dc=com").first
31+
end
32+
33+
assert_equal "user3", nested_entry.uid.first
34+
assert_equal %w(user1 user2), entries
35+
end
36+
37+
def test_nested_search_with_open
38+
entries = []
39+
nested_entry = nil
40+
41+
@ldap.open do
42+
@ldap.search(filter: "(|(uid=user1)(uid=user2))", base: "ou=People,dc=rubyldap,dc=com") do |entry|
43+
entries << entry.uid.first
44+
nested_entry ||= @ldap.search(filter: "uid=user3", base: "ou=People,dc=rubyldap,dc=com").first
45+
end
46+
end
47+
48+
assert_equal "user3", nested_entry.uid.first
49+
assert_equal %w(user1 user2), entries
50+
end
51+
end

test/integration/test_return_codes.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ def test_protocol_error
2121
end
2222

2323
def test_time_limit_exceeded
24-
refute @ldap.search(filter: "cn=timeLimitExceeded", base: "ou=Retcodes,dc=rubyldap,dc=com")
24+
assert @ldap.search(filter: "cn=timeLimitExceeded", base: "ou=Retcodes,dc=rubyldap,dc=com")
2525
assert result = @ldap.get_operation_result
2626

2727
assert_equal 3, result.code
2828
assert_equal Net::LDAP::ResultStrings[3], result.message
2929
end
3030

3131
def test_size_limit_exceeded
32-
refute @ldap.search(filter: "cn=sizeLimitExceeded", base: "ou=Retcodes,dc=rubyldap,dc=com")
32+
assert @ldap.search(filter: "cn=sizeLimitExceeded", base: "ou=Retcodes,dc=rubyldap,dc=com")
3333
assert result = @ldap.get_operation_result
3434

3535
assert_equal 4, result.code

test/integration/test_search.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,30 @@ def test_search_attributes_only
8383

8484
assert_empty entry[:cn], "unexpected attribute value: #{entry[:cn]}"
8585
end
86+
87+
def test_search_timeout
88+
entries = []
89+
events = @service.subscribe "search.net_ldap_connection"
90+
91+
result = @ldap.search(base: "dc=rubyldap,dc=com", time: 5) do |entry|
92+
assert_kind_of Net::LDAP::Entry, entry
93+
entries << entry
94+
end
95+
96+
payload, _ = events.pop
97+
assert_equal 5, payload[:time]
98+
assert_equal entries, result
99+
end
100+
101+
def test_search_with_size
102+
entries = []
103+
104+
result = @ldap.search(filter: "(uid=user1)", base: "dc=rubyldap,dc=com", size: 1) do |entry|
105+
assert_kind_of Net::LDAP::Entry, entry
106+
entries << entry
107+
end
108+
109+
refute entries.empty?
110+
assert_equal entries, result
111+
end
86112
end

test/test_ldap.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,21 @@ def test_instrument_search
4040
assert_equal [entry], payload[:result]
4141
assert_equal "(uid=user1)", payload[:filter]
4242
end
43+
44+
def test_instrument_search_with_size
45+
events = @service.subscribe "search.net_ldap"
46+
47+
flexmock(@connection).should_receive(:bind).and_return(flexmock(:bind_result, :result_code => 0))
48+
flexmock(@connection).should_receive(:search).with(Hash, Proc).
49+
yields(entry = Net::LDAP::Entry.new("uid=user1,ou=users,dc=example,dc=com")).
50+
and_return(flexmock(:search_result, :success? => true, :result_code => 4))
51+
52+
refute_nil @subject.search(:filter => "(uid=user1)", :size => 1)
53+
54+
payload, result = events.pop
55+
assert_equal [entry], result
56+
assert_equal [entry], payload[:result]
57+
assert_equal "(uid=user1)", payload[:filter]
58+
assert_equal result.size, payload[:size]
59+
end
4360
end

0 commit comments

Comments
 (0)