diff --git a/.gitignore b/.gitignore index a323762..1959fc0 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ publish/ coverage/ coverage.info .rake_tasks~ +Gemfile.lock diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..e45e65f --- /dev/null +++ b/Gemfile @@ -0,0 +1,2 @@ +source :rubygems +gemspec diff --git a/lib/net/ber/core_ext/array.rb b/lib/net/ber/core_ext/array.rb index 8fa12c1..250fa24 100644 --- a/lib/net/ber/core_ext/array.rb +++ b/lib/net/ber/core_ext/array.rb @@ -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 diff --git a/lib/net/ber/core_ext/string.rb b/lib/net/ber/core_ext/string.rb index 28aeedd..d52d787 100644 --- a/lib/net/ber/core_ext/string.rb +++ b/lib/net/ber/core_ext/string.rb @@ -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 diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 0c79b92..d154f90 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -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 diff --git a/lib/net/ldap/pdu.rb b/lib/net/ldap/pdu.rb index 3bd1f48..26d4f8b 100644 --- a/lib/net/ldap/pdu.rb +++ b/lib/net/ldap/pdu.rb @@ -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. #-- diff --git a/net-ldap.gemspec b/net-ldap.gemspec index 7605d37..34b71a8 100644 --- a/net-ldap.gemspec +++ b/net-ldap.gemspec @@ -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"] diff --git a/spec/unit/ber/core_ext/array_spec.rb b/spec/unit/ber/core_ext/array_spec.rb new file mode 100644 index 0000000..c8a6b4e --- /dev/null +++ b/spec/unit/ber/core_ext/array_spec.rb @@ -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 diff --git a/spec/unit/ldap/search_spec.rb b/spec/unit/ldap/search_spec.rb index 61cb4fd..8f9446c 100644 --- a/spec/unit/ldap/search_spec.rb +++ b/spec/unit/ldap/search_spec.rb @@ -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 diff --git a/spec/unit/ldap_spec.rb b/spec/unit/ldap_spec.rb index 1edb5c9..272d4ee 100644 --- a/spec/unit/ldap_spec.rb +++ b/spec/unit/ldap_spec.rb @@ -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 \ No newline at end of file + + 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