master
Rory OConnell 2012-02-28 20:48:31 -08:00
commit 4ab764558f
10 changed files with 191 additions and 47 deletions

1
.gitignore vendored
View File

@ -9,3 +9,4 @@ publish/
coverage/
coverage.info
.rake_tasks~
Gemfile.lock

2
Gemfile Normal file
View File

@ -0,0 +1,2 @@
source :rubygems
gemspec

View File

@ -79,4 +79,18 @@ module Net::BER::Extensions::Array
oid = ary.pack("w*")
[6, oid.length].pack("CC") + oid
end
##
# Converts an array into a set of ber control codes
# The expected format is [[control_oid, criticality, control_value(optional)]]
# [['1.2.840.113556.1.4.805',true]]
#
def to_ber_control
#if our array does not contain at least one array then wrap it in an array before going forward
ary = self[0].kind_of?(Array) ? self : [self]
ary = ary.collect do |control_sequence|
control_sequence.collect{|element| element.to_ber}.to_ber_sequence.reject_empty_ber_arrays
end
ary.to_ber_sequence.reject_empty_ber_arrays
end
end

View File

@ -46,15 +46,19 @@ module Net::BER::Extensions::String
def read_ber(syntax = nil)
StringIO.new(self).read_ber(syntax)
end
##
# Destructively reads a BER object from the string.
# Destructively reads a BER object from the string.
def read_ber!(syntax = nil)
io = StringIO.new(self)
result = io.read_ber(syntax)
self.slice!(0...io.pos)
return result
end
def reject_empty_ber_arrays
self.gsub(/0\000/n,'')
end
end

View File

@ -335,8 +335,11 @@ class Net::LDAP
68 => "Entry Already Exists"
}
module LdapControls
PagedResults = "1.2.840.113556.1.4.319" # Microsoft evil from RFC 2696
module LDAPControls
PAGED_RESULTS = "1.2.840.113556.1.4.319" # Microsoft evil from RFC 2696
SORT_REQUEST = "1.2.840.113556.1.4.473"
SORT_RESPONSE = "1.2.840.113556.1.4.474"
DELETE_TREE = "1.2.840.113556.1.4.805"
end
def self.result2string(code) #:nodoc:
@ -629,11 +632,10 @@ class Net::LDAP
yield entry if block_given?
}
else
@result = 0
begin
conn = Net::LDAP::Connection.new(:host => @host, :port => @port,
:encryption => @encryption)
if (@result = conn.bind(args[:auth] || @auth)) == 0
if (@result = conn.bind(args[:auth] || @auth)).result_code == 0
@result = conn.search(args) { |entry|
result_set << entry if result_set
yield entry if block_given?
@ -645,9 +647,9 @@ class Net::LDAP
end
if return_result_set
@result == 0 ? result_set : nil
(!@result.nil? && @result.result_code == 0) ? result_set : nil
else
@result == 0
@result
end
end
@ -721,7 +723,7 @@ class Net::LDAP
end
end
@result == 0
@result
end
# #bind_as is for testing authentication credentials.
@ -816,14 +818,14 @@ class Net::LDAP
begin
conn = Connection.new(:host => @host, :port => @port,
:encryption => @encryption)
if (@result = conn.bind(args[:auth] || @auth)) == 0
if (@result = conn.bind(args[:auth] || @auth)).result_code == 0
@result = conn.add(args)
end
ensure
conn.close if conn
end
end
@result == 0
@result
end
# Modifies the attribute values of a particular entry on the LDAP
@ -914,14 +916,15 @@ class Net::LDAP
begin
conn = Connection.new(:host => @host, :port => @port,
:encryption => @encryption)
if (@result = conn.bind(args[:auth] || @auth)) == 0
if (@result = conn.bind(args[:auth] || @auth)).result_code == 0
@result = conn.modify(args)
end
ensure
conn.close if conn
end
end
@result == 0
@result
end
# Add a value to an attribute. Takes the full DN of the entry to modify,
@ -985,14 +988,14 @@ class Net::LDAP
begin
conn = Connection.new(:host => @host, :port => @port,
:encryption => @encryption)
if (@result = conn.bind(args[:auth] || @auth)) == 0
if (@result = conn.bind(args[:auth] || @auth)).result_code == 0
@result = conn.rename(args)
end
ensure
conn.close if conn
end
end
@result == 0
@result
end
alias_method :modify_rdn, :rename
@ -1013,16 +1016,29 @@ class Net::LDAP
begin
conn = Connection.new(:host => @host, :port => @port,
:encryption => @encryption)
if (@result = conn.bind(args[:auth] || @auth)) == 0
if (@result = conn.bind(args[:auth] || @auth)).result_code == 0
@result = conn.delete(args)
end
ensure
conn.close
end
end
@result == 0
@result
end
# Delete an entry from the LDAP directory along with all subordinate entries.
# the regular delete method will fail to delete an entry if it has subordinate
# entries. This method sends an extra control code to tell the LDAP server
# to do a tree delete. ('1.2.840.113556.1.4.805')
#
# Returns True or False to indicate whether the delete succeeded. Extended
# status information is available by calling #get_operation_result.
#
# dn = "mail=deleteme@example.com, ou=people, dc=example, dc=com"
# ldap.delete_tree :dn => dn
def delete_tree(args)
delete(args.merge(:control_codes => [[Net::LDAP::LDAPControls::DELETE_TREE, true]]))
end
# This method is experimental and subject to change. Return the rootDSE
# record from the LDAP server as a Net::LDAP::Entry, or an empty Entry if
# the server doesn't return the record.
@ -1093,7 +1109,7 @@ class Net::LDAP
#++
def paged_searches_supported?
@server_caps ||= search_root_dse
@server_caps[:supportedcontrol].include?(Net::LDAP::LdapControls::PagedResults)
@server_caps[:supportedcontrol].include?(Net::LDAP::LDAPControls::PAGED_RESULTS)
end
end # class LDAP
@ -1237,7 +1253,7 @@ class Net::LDAP::Connection #:nodoc:
(be = @conn.read_ber(Net::LDAP::AsnSyntax) and pdu = Net::LDAP::PDU.new(be)) or raise Net::LDAP::LdapError, "no bind result"
pdu.result_code
pdu
end
#--
@ -1275,7 +1291,7 @@ class Net::LDAP::Connection #:nodoc:
@conn.write request_pkt
(be = @conn.read_ber(Net::LDAP::AsnSyntax) and pdu = Net::LDAP::PDU.new(be)) or raise Net::LDAP::LdapError, "no bind result"
return pdu.result_code unless pdu.result_code == 14 # saslBindInProgress
return pdu unless pdu.result_code == 14 # saslBindInProgress
raise Net::LDAP::LdapError, "sasl-challenge overflow" if ((n += 1) > MaxSaslChallenges)
cred = chall.call(pdu.result_server_sasl_creds)
@ -1315,6 +1331,35 @@ class Net::LDAP::Connection #:nodoc:
end
private :bind_gss_spnego
#--
# Allow the caller to specify a sort control
#
# The format of the sort control needs to be:
#
# :sort_control => ["cn"] # just a string
# or
# :sort_control => [["cn", "matchingRule", true]] #attribute, matchingRule, direction (true / false)
# or
# :sort_control => ["givenname","sn"] #multiple strings or arrays
#
def encode_sort_controls(sort_definitions)
return sort_definitions unless sort_definitions
sort_control_values = sort_definitions.map do |control|
control = Array(control) # if there is only an attribute name as a string then infer the orderinrule and reverseorder
control[0] = String(control[0]).to_ber,
control[1] = String(control[1]).to_ber,
control[2] = (control[2] == true).to_ber
control.to_ber_sequence
end
sort_control = [
Net::LDAP::LDAPControls::SORT_REQUEST.to_ber,
false.to_ber,
sort_control_values.to_ber_sequence.to_s.to_ber
].to_ber_sequence
end
#--
# Alternate implementation, this yields each search entry to the caller as
# it are received.
@ -1340,6 +1385,7 @@ class Net::LDAP::Connection #:nodoc:
scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree
raise Net::LDAP::LdapError, "invalid search scope" unless Net::LDAP::SearchScopes.include?(scope)
sort_control = encode_sort_controls(args.fetch(:sort_controls){ false })
# An interesting value for the size limit would be close to A/D's
# built-in page limit of 1000 records, but openLDAP newer than version
# 2.2.0 chokes on anything bigger than 126. You get a silent error that
@ -1361,7 +1407,7 @@ class Net::LDAP::Connection #:nodoc:
# to do a root-DSE record search and not do a paged search if the LDAP
# doesn't support it. Yuck.
rfc2696_cookie = [126, ""]
result_code = 0
result_pdu = nil
n_results = 0
loop {
@ -1390,17 +1436,18 @@ class Net::LDAP::Connection #:nodoc:
controls = []
controls <<
[
Net::LDAP::LdapControls::PagedResults.to_ber,
Net::LDAP::LDAPControls::PAGED_RESULTS.to_ber,
# Criticality MUST be false to interoperate with normal LDAPs.
false.to_ber,
rfc2696_cookie.map{ |v| v.to_ber}.to_ber_sequence.to_s.to_ber
].to_ber_sequence if paged_searches_supported
controls << sort_control if sort_control
controls = controls.empty? ? nil : controls.to_ber_contextspecific(0)
pkt = [next_msgid.to_ber, request, controls].compact.to_ber_sequence
@conn.write pkt
result_code = 0
result_pdu = nil
controls = []
while (be = @conn.read_ber(Net::LDAP::AsnSyntax)) && (pdu = Net::LDAP::PDU.new(be))
@ -1417,7 +1464,7 @@ class Net::LDAP::Connection #:nodoc:
end
end
when 5 # search-result
result_code = pdu.result_code
result_pdu = pdu
controls = pdu.result_controls
if return_referrals && result_code == 10
if block_given?
@ -1443,9 +1490,9 @@ class Net::LDAP::Connection #:nodoc:
# of type OCTET STRING, covered in the default syntax supported by
# read_ber, so I guess we're ok.
more_pages = false
if result_code == 0 and controls
if result_pdu.result_code == 0 and controls
controls.each do |c|
if c.oid == Net::LDAP::LdapControls::PagedResults
if c.oid == Net::LDAP::LDAPControls::PAGED_RESULTS
# just in case some bogus server sends us more than 1 of these.
more_pages = false
if c.value and c.value.length > 0
@ -1462,7 +1509,7 @@ class Net::LDAP::Connection #:nodoc:
break unless more_pages
} # loop
result_code
result_pdu || OpenStruct.new(:status => :failure, :result_code => 1, :message => "Invalid search")
end
MODIFY_OPERATIONS = { #:nodoc:
@ -1502,7 +1549,8 @@ class Net::LDAP::Connection #:nodoc:
@conn.write pkt
(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"
pdu.result_code
pdu
end
#--
@ -1523,8 +1571,12 @@ class Net::LDAP::Connection #:nodoc:
pkt = [next_msgid.to_ber, request].to_ber_sequence
@conn.write pkt
(be = @conn.read_ber(Net::LDAP::AsnSyntax)) && (pdu = Net::LDAP::PDU.new(be)) && (pdu.app_tag == 9) or raise Net::LDAP::LdapError, "response missing or invalid"
pdu.result_code
(be = @conn.read_ber(Net::LDAP::AsnSyntax)) &&
(pdu = Net::LDAP::PDU.new(be)) &&
(pdu.app_tag == 9) or
raise Net::LDAP::LdapError, "response missing or invalid"
pdu
end
#--
@ -1545,7 +1597,8 @@ class Net::LDAP::Connection #:nodoc:
(be = @conn.read_ber(Net::LDAP::AsnSyntax)) &&
(pdu = Net::LDAP::PDU.new( be )) && (pdu.app_tag == 13) or
raise Net::LDAP::LdapError.new( "response missing or invalid" )
pdu.result_code
pdu
end
#--
@ -1553,12 +1606,13 @@ class Net::LDAP::Connection #:nodoc:
#++
def delete(args)
dn = args[:dn] or raise "Unable to delete empty DN"
controls = args.include?(:control_codes) ? args[:control_codes].to_ber_control : nil #use nil so we can compact later
request = dn.to_s.to_ber_application_string(10)
pkt = [next_msgid.to_ber, request].to_ber_sequence
pkt = [next_msgid.to_ber, request, controls].compact.to_ber_sequence
@conn.write pkt
(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"
pdu.result_code
pdu
end
end # class Connection

View File

@ -112,6 +112,10 @@ class Net::LDAP::PDU
@ldap_result || {}
end
def error_message
result[:errorMessage] || ""
end
##
# This returns an LDAP result code taken from the PDU, but it will be nil
# if there wasn't a result code. That can easily happen depending on the
@ -120,6 +124,18 @@ class Net::LDAP::PDU
@ldap_result and @ldap_result[code]
end
def status
result_code == 0 ? :success : :failure
end
def success?
status == :success
end
def failure?
!success?
end
##
# Return serverSaslCreds, which are only present in BindResponse packets.
#--

View File

@ -1,7 +1,7 @@
# -*- encoding: utf-8 -*-
Gem::Specification.new do |s|
s.name = %q{net-ldap}
s.version = "0.3.1"
s.version = "0.3.2"
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["Francis Cianfrocca", "Emiel van de Laar", "Rory O'Connell", "Kaspar Schiess", "Austin Ziegler"]

View File

@ -0,0 +1,24 @@
require 'spec_helper'
require 'metaid'
describe Array, "when extended with BER core extensions" do
it "should correctly convert a control code array" do
control_codes = []
control_codes << ['1.2.3'.to_ber, true.to_ber].to_ber_sequence
control_codes << ['1.7.9'.to_ber, false.to_ber].to_ber_sequence
control_codes = control_codes.to_ber_sequence
res = [['1.2.3', true],['1.7.9',false]].to_ber_control
res.should eq(control_codes)
end
it "should wrap the array in another array if a nested array is not passed" do
result1 = ['1.2.3', true].to_ber_control
result2 = [['1.2.3', true]].to_ber_control
result1.should eq(result2)
end
it "should return an empty string if an empty array is passed" do
[].to_ber_control.should be_empty
end
end

View File

@ -3,8 +3,7 @@
describe Net::LDAP, "search method" do
class FakeConnection
def search(args)
error_code = 1
return error_code
OpenStruct.new(:result_code => 1, :message => "error")
end
end
@ -22,8 +21,8 @@ describe Net::LDAP, "search method" do
context "when :return_result => false" do
it "should return false upon error" do
success = @connection.search(:return_result => false)
success.should == false
result = @connection.search(:return_result => false)
result.result_code.should == 1
end
end

View File

@ -7,11 +7,11 @@ describe Net::LDAP::Connection do
flexmock(TCPSocket).
should_receive(:new).and_raise(Errno::ECONNREFUSED)
end
it "should raise LdapError" do
lambda {
Net::LDAP::Connection.new(
:server => 'test.mocked.com',
:server => 'test.mocked.com',
:port => 636)
}.should raise_error(Net::LDAP::LdapError)
end
@ -21,11 +21,11 @@ describe Net::LDAP::Connection do
flexmock(TCPSocket).
should_receive(:new).and_raise(SocketError)
end
it "should raise LdapError" do
lambda {
Net::LDAP::Connection.new(
:server => 'test.mocked.com',
:server => 'test.mocked.com',
:port => 636)
}.should raise_error(Net::LDAP::LdapError)
end
@ -35,14 +35,44 @@ describe Net::LDAP::Connection do
flexmock(TCPSocket).
should_receive(:new).and_raise(NameError)
end
it "should rethrow the exception" do
lambda {
Net::LDAP::Connection.new(
:server => 'test.mocked.com',
:server => 'test.mocked.com',
:port => 636)
}.should raise_error(NameError)
end
end
end
end
context "populate error messages" do
before do
@tcp_socket = flexmock(:connection)
@tcp_socket.should_receive(:write)
flexmock(TCPSocket).should_receive(:new).and_return(@tcp_socket)
end
subject { Net::LDAP::Connection.new(:server => 'test.mocked.com', :port => 636) }
it "should get back error messages if operation fails" do
ber = Net::BER::BerIdentifiedArray.new([53, "", "The provided password value was rejected by a password validator: The provided password did not contain enough characters from the character set 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'. The minimum number of characters from that set that must be present in user passwords is 1"])
ber.ber_identifier = 7
@tcp_socket.should_receive(:read_ber).and_return([2, ber])
result = subject.modify(:dn => "1", :operations => [[:replace, "mail", "something@sothsdkf.com"]])
result.should be_failure
result.error_message.should == "The provided password value was rejected by a password validator: The provided password did not contain enough characters from the character set 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'. The minimum number of characters from that set that must be present in user passwords is 1"
end
it "shouldn't get back error messages if operation succeeds" do
ber = Net::BER::BerIdentifiedArray.new([0, "", ""])
ber.ber_identifier = 7
@tcp_socket.should_receive(:read_ber).and_return([2, ber])
result = subject.modify(:dn => "1", :operations => [[:replace, "mail", "something@sothsdkf.com"]])
result.should be_success
result.error_message.should == ""
end
end
end