diff --git a/History.rdoc b/History.rdoc index 919eaf67..d49cf6a1 100644 --- a/History.rdoc +++ b/History.rdoc @@ -1,3 +1,8 @@ +=== Net::LDAP (Unreleased) +* Enforce size limit when paged searches are enabled + When paging is enabled and the requested size exceeds 126, the client now + correctly stops fetching additional pages once the size limit is reached. + === Net::LDAP 0.20.0 * Update test.yml by @HarlemSquirrel in #433 * Add `ostruct` as a dependency to the gemspec by @Ivanov-Anton in #432 diff --git a/lib/net/ldap/connection.rb b/lib/net/ldap/connection.rb index f1a70b18..376cf848 100644 --- a/lib/net/ldap/connection.rb +++ b/lib/net/ldap/connection.rb @@ -510,6 +510,11 @@ def search(args = nil) end end + # Stop paging if we've reached the caller's requested size limit. + # When paging is enabled, we set query_limit=0 (no server-side limit) + # for efficiency, so we must enforce the limit client-side here. + break if size > 0 && n_results >= size + break unless more_pages end # loop diff --git a/test/test_ldap_connection.rb b/test/test_ldap_connection.rb index fdfa418c..74ab486c 100644 --- a/test/test_ldap_connection.rb +++ b/test/test_ldap_connection.rb @@ -353,6 +353,44 @@ def test_invalid_pdu_type Net::LDAP::PDU.new([0, ber]) end end + + # Test that size limit is enforced client-side when paging is enabled. + def test_search_size_limit_stops_paging_early + make_search_entry = lambda { |entry_dn| + ber = Net::BER::BerIdentifiedArray.new([1, [entry_dn, [["uid", ["user"]]]]]) + ber.ber_identifier = Net::LDAP::PDU::SearchReturnedData + [1, ber] + } + + make_search_result = lambda { |cookie = nil| + ber = Net::BER::BerIdentifiedArray.new([Net::LDAP::ResultCodeSuccess, "", ""]) + ber.ber_identifier = Net::LDAP::PDU::SearchResult + return [1, ber] if cookie.nil? + + paging_value = "\x30".b + (126.to_ber + cookie.to_ber).then { |s| s.length.chr + s } + controls = [[Net::LDAP::LDAPControls::PAGED_RESULTS, false, paging_value]] + [1, ber, controls] + } + + # Page 1: 5 entries + result with "more" cookie + # Page 2: 5 entries + result with empty cookie (shouldn't be reached) + page1 = 5.times.map { |i| make_search_entry.call("uid=user#{i}") } + [make_search_result.call("more")] + page2 = 5.times.map { |i| make_search_entry.call("uid=user#{i + 5}") } + [make_search_result.call] + + mock = flexmock("socket") + write_count = 0 + mock.should_receive(:write).and_return { write_count += 1 } + mock.should_receive(:read_ber).and_return(*(page1 + page2)) + + conn = Net::LDAP::Connection.new(socket: mock) + results = [] + conn.search(base: "dc=test", size: 3, paged_searches_supported: true) { |e| results << e } + + # With fix: 5 entries >= size 3, stops after page 1 + # Without fix: would fetch page 2 and get 10 entries + assert_equal 5, results.size + assert_equal 1, write_count, "Should stop paging when size limit reached" + end end class TestLDAPConnectionErrors < Test::Unit::TestCase