diff --git a/.travis.yml b/.travis.yml index 6877e706..4dd7b6dc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,9 +2,11 @@ language: ruby rvm: - 1.9.3 - 2.0.0 + - 2.1.2 - jruby-19mode - rbx-19mode + # - rbx-2 matrix: allow_failures: - rvm: jruby-19mode -script: bundle exec rake spec +script: bundle exec rake diff --git a/Manifest.txt b/Manifest.txt index bea51c0c..9c81b63c 100644 --- a/Manifest.txt +++ b/Manifest.txt @@ -32,7 +32,6 @@ lib/net/ldap/version.rb lib/net/snmp.rb net-ldap.gemspec spec/integration/ssl_ber_spec.rb -spec/spec.opts spec/spec_helper.rb spec/unit/ber/ber_spec.rb spec/unit/ber/core_ext/array_spec.rb diff --git a/lib/net/ldap.rb b/lib/net/ldap.rb index 8201d8f0..cbd7917c 100644 --- a/lib/net/ldap.rb +++ b/lib/net/ldap.rb @@ -24,6 +24,7 @@ class LDAP require 'net/ldap/password' require 'net/ldap/entry' require 'net/ldap/instrumentation' +require 'net/ldap/connection' require 'net/ldap/version' # == Quick-start for the Impatient @@ -485,9 +486,9 @@ def authenticate(username, password) # standard port for simple-TLS encrypted connections is 636. Be sure you # are using the correct port. # - # [Note: a future version of Net::LDAP will support the STARTTLS LDAP - # control, which will enable encrypted communications on the same TCP port - # used for unencrypted connections.] + # The :start_tls like the :simple_tls encryption method also encrypts all + # communcations with the LDAP server. With the exception that it operates + # over the standard TCP port. def encryption(args) case args when :simple_tls, :start_tls @@ -894,9 +895,10 @@ def add(args) # operations in order. # # Each of the operations appearing in the Array must itself be an Array - # with exactly three elements: an operator:: must be :add, :replace, or - # :delete an attribute name:: the attribute name (string or symbol) to - # modify a value:: either a string or an array of strings. + # with exactly three elements: + # an operator :: must be :add, :replace, or :delete + # an attribute name :: the attribute name (string or symbol) to modify + # a value :: either a string or an array of strings. # # The :add operator will, unsurprisingly, add the specified values to the # specified attribute. If the attribute does not already exist, :add will @@ -939,13 +941,13 @@ def add(args) # may not get extended information that will tell you which one failed. # #modify has no notion of an atomic transaction. If you specify a chain # of modifications in one call to #modify, and one of them fails, the - # preceding ones will usually not be "rolled back, " resulting in a + # preceding ones will usually not be "rolled back", resulting in a # partial update. This is a limitation of the LDAP protocol, not of # Net::LDAP. # # The lack of transactional atomicity in LDAP means that you're usually # better off using the convenience methods #add_attribute, - # #replace_attribute, and #delete_attribute, which are are wrappers over + # #replace_attribute, and #delete_attribute, which are wrappers over # #modify. However, certain LDAP servers may provide concurrency # semantics, in which the several operations contained in a single #modify # call are not interleaved with other modification-requests received @@ -1174,577 +1176,3 @@ def paged_searches_supported? @server_caps[:supportedcontrol].include?(Net::LDAP::LDAPControls::PAGED_RESULTS) end end # class LDAP - -# This is a private class used internally by the library. It should not -# be called by user code. -class Net::LDAP::Connection #:nodoc: - include Net::LDAP::Instrumentation - - LdapVersion = 3 - MaxSaslChallenges = 10 - - def initialize(server) - @instrumentation_service = server[:instrumentation_service] - - begin - @conn = TCPSocket.new(server[:host], server[:port]) - rescue SocketError - raise Net::LDAP::LdapError, "No such address or other socket error." - rescue Errno::ECONNREFUSED - raise Net::LDAP::LdapError, "Server #{server[:host]} refused connection on port #{server[:port]}." - end - - if server[:encryption] - setup_encryption server[:encryption] - end - - yield self if block_given? - end - - module GetbyteForSSLSocket - def getbyte - getc.ord - end - end - - module FixSSLSocketSyncClose - def close - super - io.close - end - end - - def self.wrap_with_ssl(io) - raise Net::LDAP::LdapError, "OpenSSL is unavailable" unless Net::LDAP::HasOpenSSL - ctx = OpenSSL::SSL::SSLContext.new - conn = OpenSSL::SSL::SSLSocket.new(io, ctx) - conn.connect - - # Doesn't work: - # conn.sync_close = true - - conn.extend(GetbyteForSSLSocket) unless conn.respond_to?(:getbyte) - conn.extend(FixSSLSocketSyncClose) - - conn - end - - #-- - # Helper method called only from new, and only after we have a - # successfully-opened @conn instance variable, which is a TCP connection. - # Depending on the received arguments, we establish SSL, potentially - # replacing the value of @conn accordingly. Don't generate any errors here - # if no encryption is requested. DO raise Net::LDAP::LdapError objects if encryption - # is requested and we have trouble setting it up. That includes if OpenSSL - # is not set up on the machine. (Question: how does the Ruby OpenSSL - # wrapper react in that case?) DO NOT filter exceptions raised by the - # OpenSSL library. Let them pass back to the user. That should make it - # easier for us to debug the problem reports. Presumably (hopefully?) that - # will also produce recognizable errors if someone tries to use this on a - # machine without OpenSSL. - # - # The simple_tls method is intended as the simplest, stupidest, easiest - # solution for people who want nothing more than encrypted comms with the - # LDAP server. It doesn't do any server-cert validation and requires - # nothing in the way of key files and root-cert files, etc etc. OBSERVE: - # WE REPLACE the value of @conn, which is presumed to be a connected - # TCPSocket object. - # - # The start_tls method is supported by many servers over the standard LDAP - # port. It does not require an alternative port for encrypted - # communications, as with simple_tls. Thanks for Kouhei Sutou for - # generously contributing the :start_tls path. - #++ - def setup_encryption(args) - case args[:method] - when :simple_tls - @conn = self.class.wrap_with_ssl(@conn) - # additional branches requiring server validation and peer certs, etc. - # go here. - when :start_tls - msgid = next_msgid.to_ber - request = [Net::LDAP::StartTlsOid.to_ber_contextspecific(0)].to_ber_appsequence(Net::LDAP::PDU::ExtendedRequest) - request_pkt = [msgid, request].to_ber_sequence - write request_pkt - be = read - raise Net::LDAP::LdapError, "no start_tls result" if be.nil? - pdu = Net::LDAP::PDU.new(be) - raise Net::LDAP::LdapError, "no start_tls result" if pdu.nil? - if pdu.result_code.zero? - @conn = self.class.wrap_with_ssl(@conn) - else - raise Net::LDAP::LdapError, "start_tls failed: #{pdu.result_code}" - end - else - raise Net::LDAP::LdapError, "unsupported encryption method #{args[:method]}" - end - end - - #-- - # This is provided as a convenience method to make sure a connection - # object gets closed without waiting for a GC to happen. Clients shouldn't - # have to call it, but perhaps it will come in handy someday. - #++ - def close - @conn.close - @conn = nil - end - - # Internal: Reads and parses data from the configured connection. - # - # - syntax: the BER syntax to use to parse the read data with - # - # Returns basic BER objects. - def read(syntax = Net::LDAP::AsnSyntax) - instrument "read.net_ldap_connection", :syntax => syntax do |payload| - @conn.read_ber(syntax) do |id, content_length| - payload[:object_type_id] = id - payload[:content_length] = content_length - end - end - end - private :read - - # Internal: Writes the given packet to the configured connection. - # - # - packet: the BER data packet to write on the socket. - # - # Returns the return value from writing to the connection, which in some - # cases is the Integer number of bytes written to the socket. - def write(packet) - instrument "write.net_ldap_connection" do |payload| - payload[:content_length] = @conn.write(packet) - end - end - private :write - - def next_msgid - @msgid ||= 0 - @msgid += 1 - end - - def bind(auth) - instrument "bind.net_ldap_connection" do |payload| - payload[:method] = meth = auth[:method] - if [:simple, :anonymous, :anon].include?(meth) - bind_simple auth - elsif meth == :sasl - bind_sasl(auth) - elsif meth == :gss_spnego - bind_gss_spnego(auth) - else - raise Net::LDAP::LdapError, "Unsupported auth method (#{meth})" - end - end - end - - #-- - # Implements a simple user/psw authentication. Accessed by calling #bind - # with a method of :simple or :anonymous. - #++ - def bind_simple(auth) - user, psw = if auth[:method] == :simple - [auth[:username] || auth[:dn], auth[:password]] - else - ["", ""] - end - - raise Net::LDAP::LdapError, "Invalid binding information" unless (user && psw) - - msgid = next_msgid.to_ber - request = [LdapVersion.to_ber, user.to_ber, - psw.to_ber_contextspecific(0)].to_ber_appsequence(0) - request_pkt = [msgid, request].to_ber_sequence - write request_pkt - - (be = read and pdu = Net::LDAP::PDU.new(be)) or raise Net::LDAP::LdapError, "no bind result" - - pdu - end - - #-- - # Required parameters: :mechanism, :initial_credential and - # :challenge_response - # - # Mechanism is a string value that will be passed in the SASL-packet's - # "mechanism" field. - # - # Initial credential is most likely a string. It's passed in the initial - # BindRequest that goes to the server. In some protocols, it may be empty. - # - # Challenge-response is a Ruby proc that takes a single parameter and - # returns an object that will typically be a string. The - # challenge-response block is called when the server returns a - # BindResponse with a result code of 14 (saslBindInProgress). The - # challenge-response block receives a parameter containing the data - # returned by the server in the saslServerCreds field of the LDAP - # BindResponse packet. The challenge-response block may be called multiple - # times during the course of a SASL authentication, and each time it must - # return a value that will be passed back to the server as the credential - # data in the next BindRequest packet. - #++ - def bind_sasl(auth) - mech, cred, chall = auth[:mechanism], auth[:initial_credential], - auth[:challenge_response] - raise Net::LDAP::LdapError, "Invalid binding information" unless (mech && cred && chall) - - n = 0 - loop { - msgid = next_msgid.to_ber - sasl = [mech.to_ber, cred.to_ber].to_ber_contextspecific(3) - request = [LdapVersion.to_ber, "".to_ber, sasl].to_ber_appsequence(0) - request_pkt = [msgid, request].to_ber_sequence - write request_pkt - - (be = read and pdu = Net::LDAP::PDU.new(be)) or raise Net::LDAP::LdapError, "no bind result" - 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) - } - - raise Net::LDAP::LdapError, "why are we here?" - end - private :bind_sasl - - #-- - # PROVISIONAL, only for testing SASL implementations. DON'T USE THIS YET. - # Uses Kohei Kajimoto's Ruby/NTLM. We have to find a clean way to - # integrate it without introducing an external dependency. - # - # This authentication method is accessed by calling #bind with a :method - # parameter of :gss_spnego. It requires :username and :password - # attributes, just like the :simple authentication method. It performs a - # GSS-SPNEGO authentication with the server, which is presumed to be a - # Microsoft Active Directory. - #++ - def bind_gss_spnego(auth) - require 'ntlm' - - user, psw = [auth[:username] || auth[:dn], auth[:password]] - raise Net::LDAP::LdapError, "Invalid binding information" unless (user && psw) - - nego = proc { |challenge| - t2_msg = NTLM::Message.parse(challenge) - t3_msg = t2_msg.response({ :user => user, :password => psw }, - { :ntlmv2 => true }) - t3_msg.serialize - } - - bind_sasl(:method => :sasl, :mechanism => "GSS-SPNEGO", - :initial_credential => NTLM::Message::Type1.new.serialize, - :challenge_response => nego) - 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. - # - # TODO: certain search parameters are hardcoded. - # TODO: if we mis-parse the server results or the results are wrong, we - # can block forever. That's because we keep reading results until we get a - # type-5 packet, which might never come. We need to support the time-limit - # in the protocol. - #++ - def search(args = {}) - search_filter = (args && args[:filter]) || - Net::LDAP::Filter.eq("objectclass", "*") - search_filter = Net::LDAP::Filter.construct(search_filter) if search_filter.is_a?(String) - search_base = (args && args[:base]) || "dc=example, dc=com" - search_attributes = ((args && args[:attributes]) || []).map { |attr| attr.to_s.to_ber} - return_referrals = args && args[:return_referrals] == true - sizelimit = (args && args[:size].to_i) || 0 - raise Net::LDAP::LdapError, "invalid search-size" unless sizelimit >= 0 - paged_searches_supported = (args && args[:paged_searches_supported]) - - attributes_only = (args and args[:attributes_only] == true) - 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 }) - - deref = args[:deref] || Net::LDAP::DerefAliases_Never - raise Net::LDAP::LdapError.new( "invalid alias dereferencing value" ) unless Net::LDAP::DerefAliasesArray.include?(deref) - - - # 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 - # is easily visible by running slapd in debug mode. Go figure. - # - # Changed this around 06Sep06 to support a caller-specified search-size - # limit. Because we ALWAYS do paged searches, we have to work around the - # problem that it's not legal to specify a "normal" sizelimit (in the - # body of the search request) that is larger than the page size we're - # requesting. Unfortunately, I have the feeling that this will break - # with LDAP servers that don't support paged searches!!! - # - # (Because we pass zero as the sizelimit on search rounds when the - # remaining limit is larger than our max page size of 126. In these - # cases, I think the caller's search limit will be ignored!) - # - # CONFIRMED: This code doesn't work on LDAPs that don't support paged - # searches when the size limit is larger than 126. We're going to have - # 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_pdu = nil - n_results = 0 - - instrument "search.net_ldap_connection", - :filter => search_filter, - :base => search_base, - :scope => scope, - :limit => sizelimit, - :sort => sort_control, - :referrals => return_referrals, - :deref => deref, - :attributes => search_attributes do |payload| - loop do - # should collect this into a private helper to clarify the structure - query_limit = 0 - if sizelimit > 0 - if paged_searches_supported - query_limit = (((sizelimit - n_results) < 126) ? (sizelimit - - n_results) : 0) - else - query_limit = sizelimit - end - end - - request = [ - search_base.to_ber, - scope.to_ber_enumerated, - deref.to_ber_enumerated, - query_limit.to_ber, # size limit - 0.to_ber, - attributes_only.to_ber, - search_filter.to_ber, - search_attributes.to_ber_sequence - ].to_ber_appsequence(3) - - # rfc2696_cookie sometimes contains binary data from Microsoft Active Directory - # this breaks when calling to_ber. (Can't force binary data to UTF-8) - # we have to disable paging (even though server supports it) to get around this... - - controls = [] - controls << - [ - 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 - write pkt - - result_pdu = nil - controls = [] - - while (be = read) && (pdu = Net::LDAP::PDU.new(be)) - case pdu.app_tag - when Net::LDAP::PDU::SearchReturnedData - n_results += 1 - yield pdu.search_entry if block_given? - when Net::LDAP::PDU::SearchResultReferral - if return_referrals - if block_given? - se = Net::LDAP::Entry.new - se[:search_referrals] = (pdu.search_referrals || []) - yield se - end - end - when Net::LDAP::PDU::SearchResult - result_pdu = pdu - controls = pdu.result_controls - if return_referrals && pdu.result_code == 10 - if block_given? - se = Net::LDAP::Entry.new - se[:search_referrals] = (pdu.search_referrals || []) - yield se - end - end - break - else - raise Net::LDAP::LdapError, "invalid response-type in search: #{pdu.app_tag}" - end - end - - # count number of pages of results - payload[:page_count] ||= 0 - payload[:page_count] += 1 - - # When we get here, we have seen a type-5 response. If there is no - # error AND there is an RFC-2696 cookie, then query again for the next - # page of results. If not, we're done. Don't screw this up or we'll - # break every search we do. - # - # Noticed 02Sep06, look at the read_ber call in this loop, shouldn't - # that have a parameter of AsnSyntax? Does this just accidentally - # work? According to RFC-2696, the value expected in this position is - # of type OCTET STRING, covered in the default syntax supported by - # read_ber, so I guess we're ok. - more_pages = false - if result_pdu.result_code == 0 and controls - controls.each do |c| - 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 - cookie = c.value.read_ber[1] - if cookie and cookie.length > 0 - rfc2696_cookie[1] = cookie - more_pages = true - end - end - end - end - end - - break unless more_pages - end # loop - - # track total result count - payload[:result_count] = n_results - - result_pdu || OpenStruct.new(:status => :failure, :result_code => 1, :message => "Invalid search") - end # instrument - end - - MODIFY_OPERATIONS = { #:nodoc: - :add => 0, - :delete => 1, - :replace => 2 - } - - def self.modify_ops(operations) - ops = [] - if operations - operations.each { |op, attrib, values| - # TODO, fix the following line, which gives a bogus error if the - # opcode is invalid. - op_ber = MODIFY_OPERATIONS[op.to_sym].to_ber_enumerated - values = [ values ].flatten.map { |v| v.to_ber if v }.to_ber_set - values = [ attrib.to_s.to_ber, values ].to_ber_sequence - ops << [ op_ber, values ].to_ber - } - end - ops - end - - #-- - # TODO: need to support a time limit, in case the server fails to respond. - # TODO: We're throwing an exception here on empty DN. Should return a - # proper error instead, probaby from farther up the chain. - # TODO: If the user specifies a bogus opcode, we'll throw a confusing - # error here ("to_ber_enumerated is not defined on nil"). - #++ - def modify(args) - modify_dn = args[:dn] or raise "Unable to modify empty DN" - ops = self.class.modify_ops args[:operations] - request = [ modify_dn.to_ber, - ops.to_ber_sequence ].to_ber_appsequence(6) - pkt = [ next_msgid.to_ber, request ].to_ber_sequence - write pkt - - (be = read) && (pdu = Net::LDAP::PDU.new(be)) && (pdu.app_tag == Net::LDAP::PDU::ModifyResponse) or raise Net::LDAP::LdapError, "response missing or invalid" - - pdu - end - - #-- - # TODO: need to support a time limit, in case the server fails to respond. - # Unlike other operation-methods in this class, we return a result hash - # rather than a simple result number. This is experimental, and eventually - # we'll want to do this with all the others. The point is to have access - # to the error message and the matched-DN returned by the server. - #++ - def add(args) - add_dn = args[:dn] or raise Net::LDAP::LdapError, "Unable to add empty DN" - add_attrs = [] - a = args[:attributes] and a.each { |k, v| - add_attrs << [ k.to_s.to_ber, Array(v).map { |m| m.to_ber}.to_ber_set ].to_ber_sequence - } - - request = [add_dn.to_ber, add_attrs.to_ber_sequence].to_ber_appsequence(8) - pkt = [next_msgid.to_ber, request].to_ber_sequence - write pkt - - (be = read) && - (pdu = Net::LDAP::PDU.new(be)) && - (pdu.app_tag == Net::LDAP::PDU::AddResponse) or - raise Net::LDAP::LdapError, "response missing or invalid" - - pdu - end - - #-- - # TODO: need to support a time limit, in case the server fails to respond. - #++ - def rename(args) - old_dn = args[:olddn] or raise "Unable to rename empty DN" - new_rdn = args[:newrdn] or raise "Unable to rename to empty RDN" - delete_attrs = args[:delete_attributes] ? true : false - new_superior = args[:new_superior] - - request = [old_dn.to_ber, new_rdn.to_ber, delete_attrs.to_ber] - request << new_superior.to_ber_contextspecific(0) unless new_superior == nil - - pkt = [next_msgid.to_ber, request.to_ber_appsequence(12)].to_ber_sequence - write pkt - - (be = read) && - (pdu = Net::LDAP::PDU.new( be )) && (pdu.app_tag == Net::LDAP::PDU::ModifyRDNResponse) or - raise Net::LDAP::LdapError.new( "response missing or invalid" ) - - pdu - end - - #-- - # TODO, need to support a time limit, in case the server fails to respond. - #++ - 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, controls].compact.to_ber_sequence - write pkt - - (be = read) && (pdu = Net::LDAP::PDU.new(be)) && (pdu.app_tag == Net::LDAP::PDU::DeleteResponse) or raise Net::LDAP::LdapError, "response missing or invalid" - - pdu - end -end # class Connection diff --git a/lib/net/ldap/connection.rb b/lib/net/ldap/connection.rb new file mode 100644 index 00000000..00d21502 --- /dev/null +++ b/lib/net/ldap/connection.rb @@ -0,0 +1,573 @@ +# This is a private class used internally by the library. It should not +# be called by user code. +class Net::LDAP::Connection #:nodoc: + include Net::LDAP::Instrumentation + + LdapVersion = 3 + MaxSaslChallenges = 10 + + def initialize(server) + @instrumentation_service = server[:instrumentation_service] + + begin + @conn = TCPSocket.new(server[:host], server[:port]) + rescue SocketError + raise Net::LDAP::LdapError, "No such address or other socket error." + rescue Errno::ECONNREFUSED + raise Net::LDAP::LdapError, "Server #{server[:host]} refused connection on port #{server[:port]}." + end + + if server[:encryption] + setup_encryption server[:encryption] + end + + yield self if block_given? + end + + module GetbyteForSSLSocket + def getbyte + getc.ord + end + end + + module FixSSLSocketSyncClose + def close + super + io.close + end + end + + def self.wrap_with_ssl(io) + raise Net::LDAP::LdapError, "OpenSSL is unavailable" unless Net::LDAP::HasOpenSSL + ctx = OpenSSL::SSL::SSLContext.new + conn = OpenSSL::SSL::SSLSocket.new(io, ctx) + conn.connect + + # Doesn't work: + # conn.sync_close = true + + conn.extend(GetbyteForSSLSocket) unless conn.respond_to?(:getbyte) + conn.extend(FixSSLSocketSyncClose) + + conn + end + + #-- + # Helper method called only from new, and only after we have a + # successfully-opened @conn instance variable, which is a TCP connection. + # Depending on the received arguments, we establish SSL, potentially + # replacing the value of @conn accordingly. Don't generate any errors here + # if no encryption is requested. DO raise Net::LDAP::LdapError objects if encryption + # is requested and we have trouble setting it up. That includes if OpenSSL + # is not set up on the machine. (Question: how does the Ruby OpenSSL + # wrapper react in that case?) DO NOT filter exceptions raised by the + # OpenSSL library. Let them pass back to the user. That should make it + # easier for us to debug the problem reports. Presumably (hopefully?) that + # will also produce recognizable errors if someone tries to use this on a + # machine without OpenSSL. + # + # The simple_tls method is intended as the simplest, stupidest, easiest + # solution for people who want nothing more than encrypted comms with the + # LDAP server. It doesn't do any server-cert validation and requires + # nothing in the way of key files and root-cert files, etc etc. OBSERVE: + # WE REPLACE the value of @conn, which is presumed to be a connected + # TCPSocket object. + # + # The start_tls method is supported by many servers over the standard LDAP + # port. It does not require an alternative port for encrypted + # communications, as with simple_tls. Thanks for Kouhei Sutou for + # generously contributing the :start_tls path. + #++ + def setup_encryption(args) + case args[:method] + when :simple_tls + @conn = self.class.wrap_with_ssl(@conn) + # additional branches requiring server validation and peer certs, etc. + # go here. + when :start_tls + msgid = next_msgid.to_ber + request = [Net::LDAP::StartTlsOid.to_ber_contextspecific(0)].to_ber_appsequence(Net::LDAP::PDU::ExtendedRequest) + request_pkt = [msgid, request].to_ber_sequence + write request_pkt + be = read + raise Net::LDAP::LdapError, "no start_tls result" if be.nil? + pdu = Net::LDAP::PDU.new(be) + raise Net::LDAP::LdapError, "no start_tls result" if pdu.nil? + if pdu.result_code.zero? + @conn = self.class.wrap_with_ssl(@conn) + else + raise Net::LDAP::LdapError, "start_tls failed: #{pdu.result_code}" + end + else + raise Net::LDAP::LdapError, "unsupported encryption method #{args[:method]}" + end + end + + #-- + # This is provided as a convenience method to make sure a connection + # object gets closed without waiting for a GC to happen. Clients shouldn't + # have to call it, but perhaps it will come in handy someday. + #++ + def close + @conn.close + @conn = nil + end + + # Internal: Reads and parses data from the configured connection. + # + # - syntax: the BER syntax to use to parse the read data with + # + # Returns basic BER objects. + def read(syntax = Net::LDAP::AsnSyntax) + instrument "read.net_ldap_connection", :syntax => syntax do |payload| + @conn.read_ber(syntax) do |id, content_length| + payload[:object_type_id] = id + payload[:content_length] = content_length + end + end + end + private :read + + # Internal: Writes the given packet to the configured connection. + # + # - packet: the BER data packet to write on the socket. + # + # Returns the return value from writing to the connection, which in some + # cases is the Integer number of bytes written to the socket. + def write(packet) + instrument "write.net_ldap_connection" do |payload| + payload[:content_length] = @conn.write(packet) + end + end + private :write + + def next_msgid + @msgid ||= 0 + @msgid += 1 + end + + def bind(auth) + instrument "bind.net_ldap_connection" do |payload| + payload[:method] = meth = auth[:method] + if [:simple, :anonymous, :anon].include?(meth) + bind_simple auth + elsif meth == :sasl + bind_sasl(auth) + elsif meth == :gss_spnego + bind_gss_spnego(auth) + else + raise Net::LDAP::LdapError, "Unsupported auth method (#{meth})" + end + end + end + + #-- + # Implements a simple user/psw authentication. Accessed by calling #bind + # with a method of :simple or :anonymous. + #++ + def bind_simple(auth) + user, psw = if auth[:method] == :simple + [auth[:username] || auth[:dn], auth[:password]] + else + ["", ""] + end + + raise Net::LDAP::LdapError, "Invalid binding information" unless (user && psw) + + msgid = next_msgid.to_ber + request = [LdapVersion.to_ber, user.to_ber, + psw.to_ber_contextspecific(0)].to_ber_appsequence(0) + request_pkt = [msgid, request].to_ber_sequence + write request_pkt + + (be = read and pdu = Net::LDAP::PDU.new(be)) or raise Net::LDAP::LdapError, "no bind result" + + pdu + end + + #-- + # Required parameters: :mechanism, :initial_credential and + # :challenge_response + # + # Mechanism is a string value that will be passed in the SASL-packet's + # "mechanism" field. + # + # Initial credential is most likely a string. It's passed in the initial + # BindRequest that goes to the server. In some protocols, it may be empty. + # + # Challenge-response is a Ruby proc that takes a single parameter and + # returns an object that will typically be a string. The + # challenge-response block is called when the server returns a + # BindResponse with a result code of 14 (saslBindInProgress). The + # challenge-response block receives a parameter containing the data + # returned by the server in the saslServerCreds field of the LDAP + # BindResponse packet. The challenge-response block may be called multiple + # times during the course of a SASL authentication, and each time it must + # return a value that will be passed back to the server as the credential + # data in the next BindRequest packet. + #++ + def bind_sasl(auth) + mech, cred, chall = auth[:mechanism], auth[:initial_credential], + auth[:challenge_response] + raise Net::LDAP::LdapError, "Invalid binding information" unless (mech && cred && chall) + + n = 0 + loop { + msgid = next_msgid.to_ber + sasl = [mech.to_ber, cred.to_ber].to_ber_contextspecific(3) + request = [LdapVersion.to_ber, "".to_ber, sasl].to_ber_appsequence(0) + request_pkt = [msgid, request].to_ber_sequence + write request_pkt + + (be = read and pdu = Net::LDAP::PDU.new(be)) or raise Net::LDAP::LdapError, "no bind result" + 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) + } + + raise Net::LDAP::LdapError, "why are we here?" + end + private :bind_sasl + + #-- + # PROVISIONAL, only for testing SASL implementations. DON'T USE THIS YET. + # Uses Kohei Kajimoto's Ruby/NTLM. We have to find a clean way to + # integrate it without introducing an external dependency. + # + # This authentication method is accessed by calling #bind with a :method + # parameter of :gss_spnego. It requires :username and :password + # attributes, just like the :simple authentication method. It performs a + # GSS-SPNEGO authentication with the server, which is presumed to be a + # Microsoft Active Directory. + #++ + def bind_gss_spnego(auth) + require 'ntlm' + + user, psw = [auth[:username] || auth[:dn], auth[:password]] + raise Net::LDAP::LdapError, "Invalid binding information" unless (user && psw) + + nego = proc { |challenge| + t2_msg = NTLM::Message.parse(challenge) + t3_msg = t2_msg.response({ :user => user, :password => psw }, + { :ntlmv2 => true }) + t3_msg.serialize + } + + bind_sasl(:method => :sasl, :mechanism => "GSS-SPNEGO", + :initial_credential => NTLM::Message::Type1.new.serialize, + :challenge_response => nego) + 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. + # + # TODO: certain search parameters are hardcoded. + # TODO: if we mis-parse the server results or the results are wrong, we + # can block forever. That's because we keep reading results until we get a + # type-5 packet, which might never come. We need to support the time-limit + # in the protocol. + #++ + def search(args = {}) + search_filter = (args && args[:filter]) || + Net::LDAP::Filter.eq("objectclass", "*") + search_filter = Net::LDAP::Filter.construct(search_filter) if search_filter.is_a?(String) + search_base = (args && args[:base]) || "dc=example, dc=com" + search_attributes = ((args && args[:attributes]) || []).map { |attr| attr.to_s.to_ber} + return_referrals = args && args[:return_referrals] == true + sizelimit = (args && args[:size].to_i) || 0 + raise Net::LDAP::LdapError, "invalid search-size" unless sizelimit >= 0 + paged_searches_supported = (args && args[:paged_searches_supported]) + + attributes_only = (args and args[:attributes_only] == true) + 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 }) + + deref = args[:deref] || Net::LDAP::DerefAliases_Never + raise Net::LDAP::LdapError.new( "invalid alias dereferencing value" ) unless Net::LDAP::DerefAliasesArray.include?(deref) + + + # 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 + # is easily visible by running slapd in debug mode. Go figure. + # + # Changed this around 06Sep06 to support a caller-specified search-size + # limit. Because we ALWAYS do paged searches, we have to work around the + # problem that it's not legal to specify a "normal" sizelimit (in the + # body of the search request) that is larger than the page size we're + # requesting. Unfortunately, I have the feeling that this will break + # with LDAP servers that don't support paged searches!!! + # + # (Because we pass zero as the sizelimit on search rounds when the + # remaining limit is larger than our max page size of 126. In these + # cases, I think the caller's search limit will be ignored!) + # + # CONFIRMED: This code doesn't work on LDAPs that don't support paged + # searches when the size limit is larger than 126. We're going to have + # 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_pdu = nil + n_results = 0 + + instrument "search.net_ldap_connection", + :filter => search_filter, + :base => search_base, + :scope => scope, + :limit => sizelimit, + :sort => sort_control, + :referrals => return_referrals, + :deref => deref, + :attributes => search_attributes do |payload| + loop do + # should collect this into a private helper to clarify the structure + query_limit = 0 + if sizelimit > 0 + if paged_searches_supported + query_limit = (((sizelimit - n_results) < 126) ? (sizelimit - + n_results) : 0) + else + query_limit = sizelimit + end + end + + request = [ + search_base.to_ber, + scope.to_ber_enumerated, + deref.to_ber_enumerated, + query_limit.to_ber, # size limit + 0.to_ber, + attributes_only.to_ber, + search_filter.to_ber, + search_attributes.to_ber_sequence + ].to_ber_appsequence(3) + + # rfc2696_cookie sometimes contains binary data from Microsoft Active Directory + # this breaks when calling to_ber. (Can't force binary data to UTF-8) + # we have to disable paging (even though server supports it) to get around this... + + controls = [] + controls << + [ + 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 + write pkt + + result_pdu = nil + controls = [] + + while (be = read) && (pdu = Net::LDAP::PDU.new(be)) + case pdu.app_tag + when Net::LDAP::PDU::SearchReturnedData + n_results += 1 + yield pdu.search_entry if block_given? + when Net::LDAP::PDU::SearchResultReferral + if return_referrals + if block_given? + se = Net::LDAP::Entry.new + se[:search_referrals] = (pdu.search_referrals || []) + yield se + end + end + when Net::LDAP::PDU::SearchResult + result_pdu = pdu + controls = pdu.result_controls + if return_referrals && pdu.result_code == 10 + if block_given? + se = Net::LDAP::Entry.new + se[:search_referrals] = (pdu.search_referrals || []) + yield se + end + end + break + else + raise Net::LDAP::LdapError, "invalid response-type in search: #{pdu.app_tag}" + end + end + + # count number of pages of results + payload[:page_count] ||= 0 + payload[:page_count] += 1 + + # When we get here, we have seen a type-5 response. If there is no + # error AND there is an RFC-2696 cookie, then query again for the next + # page of results. If not, we're done. Don't screw this up or we'll + # break every search we do. + # + # Noticed 02Sep06, look at the read_ber call in this loop, shouldn't + # that have a parameter of AsnSyntax? Does this just accidentally + # work? According to RFC-2696, the value expected in this position is + # of type OCTET STRING, covered in the default syntax supported by + # read_ber, so I guess we're ok. + more_pages = false + if result_pdu.result_code == 0 and controls + controls.each do |c| + 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 + cookie = c.value.read_ber[1] + if cookie and cookie.length > 0 + rfc2696_cookie[1] = cookie + more_pages = true + end + end + end + end + end + + break unless more_pages + end # loop + + # track total result count + payload[:result_count] = n_results + + result_pdu || OpenStruct.new(:status => :failure, :result_code => 1, :message => "Invalid search") + end # instrument + end + + MODIFY_OPERATIONS = { #:nodoc: + :add => 0, + :delete => 1, + :replace => 2 + } + + def self.modify_ops(operations) + ops = [] + if operations + operations.each { |op, attrib, values| + # TODO, fix the following line, which gives a bogus error if the + # opcode is invalid. + op_ber = MODIFY_OPERATIONS[op.to_sym].to_ber_enumerated + values = [ values ].flatten.map { |v| v.to_ber if v }.to_ber_set + values = [ attrib.to_s.to_ber, values ].to_ber_sequence + ops << [ op_ber, values ].to_ber + } + end + ops + end + + #-- + # TODO: need to support a time limit, in case the server fails to respond. + # TODO: We're throwing an exception here on empty DN. Should return a + # proper error instead, probaby from farther up the chain. + # TODO: If the user specifies a bogus opcode, we'll throw a confusing + # error here ("to_ber_enumerated is not defined on nil"). + #++ + def modify(args) + modify_dn = args[:dn] or raise "Unable to modify empty DN" + ops = self.class.modify_ops args[:operations] + request = [ modify_dn.to_ber, + ops.to_ber_sequence ].to_ber_appsequence(6) + pkt = [ next_msgid.to_ber, request ].to_ber_sequence + write pkt + + (be = read) && (pdu = Net::LDAP::PDU.new(be)) && (pdu.app_tag == Net::LDAP::PDU::ModifyResponse) or raise Net::LDAP::LdapError, "response missing or invalid" + + pdu + end + + #-- + # TODO: need to support a time limit, in case the server fails to respond. + # Unlike other operation-methods in this class, we return a result hash + # rather than a simple result number. This is experimental, and eventually + # we'll want to do this with all the others. The point is to have access + # to the error message and the matched-DN returned by the server. + #++ + def add(args) + add_dn = args[:dn] or raise Net::LDAP::LdapError, "Unable to add empty DN" + add_attrs = [] + a = args[:attributes] and a.each { |k, v| + add_attrs << [ k.to_s.to_ber, Array(v).map { |m| m.to_ber}.to_ber_set ].to_ber_sequence + } + + request = [add_dn.to_ber, add_attrs.to_ber_sequence].to_ber_appsequence(8) + pkt = [next_msgid.to_ber, request].to_ber_sequence + write pkt + + (be = read) && + (pdu = Net::LDAP::PDU.new(be)) && + (pdu.app_tag == Net::LDAP::PDU::AddResponse) or + raise Net::LDAP::LdapError, "response missing or invalid" + + pdu + end + + #-- + # TODO: need to support a time limit, in case the server fails to respond. + #++ + def rename(args) + old_dn = args[:olddn] or raise "Unable to rename empty DN" + new_rdn = args[:newrdn] or raise "Unable to rename to empty RDN" + delete_attrs = args[:delete_attributes] ? true : false + new_superior = args[:new_superior] + + request = [old_dn.to_ber, new_rdn.to_ber, delete_attrs.to_ber] + request << new_superior.to_ber_contextspecific(0) unless new_superior == nil + + pkt = [next_msgid.to_ber, request.to_ber_appsequence(12)].to_ber_sequence + write pkt + + (be = read) && + (pdu = Net::LDAP::PDU.new( be )) && (pdu.app_tag == Net::LDAP::PDU::ModifyRDNResponse) or + raise Net::LDAP::LdapError.new( "response missing or invalid" ) + + pdu + end + + #-- + # TODO, need to support a time limit, in case the server fails to respond. + #++ + 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, controls].compact.to_ber_sequence + write pkt + + (be = read) && (pdu = Net::LDAP::PDU.new(be)) && (pdu.app_tag == Net::LDAP::PDU::DeleteResponse) or raise Net::LDAP::LdapError, "response missing or invalid" + + pdu + end +end # class Connection diff --git a/lib/net/ldap/dataset.rb b/lib/net/ldap/dataset.rb index ffdee11f..54fc1a07 100644 --- a/lib/net/ldap/dataset.rb +++ b/lib/net/ldap/dataset.rb @@ -4,11 +4,13 @@ # to and from LDIF strings and Net::LDAP::Entry objects. class Net::LDAP::Dataset < Hash ## - # Dataset object comments. - attr_reader :comments + # Dataset object version, comments. + attr_accessor :version + attr_reader :comments def initialize(*args, &block) # :nodoc: super + @version = nil @comments = [] end @@ -17,6 +19,12 @@ def initialize(*args, &block) # :nodoc: # entries. def to_ldif ary = [] + + if version + ary << "version: #{version}" + ary << "" + end + ary += @comments unless @comments.empty? keys.sort.each do |dn| ary << "dn: #{dn}" @@ -125,8 +133,14 @@ def read_ldif(io) if line =~ /^#/ ds.comments << line yield :comment, line if block_given? - elsif line =~ /^dn:[\s]*/i - dn = $' + elsif line =~ /^version:[\s]*([0-9]+)$/i + ds.version = $1 + yield :version, line if block_given? + elsif line =~ /^dn:([\:]?)[\s]*/i + # $1 is a colon if the dn-value is base-64 encoded + # $' is the dn-value + # Avoid the Base64 class because not all Ruby versions have it. + dn = ($1 == ":") ? $'.unpack('m').shift : $' ds[dn] = Hash.new { |k,v| k[v] = [] } yield :dn, dn if block_given? elsif line.empty? diff --git a/net-ldap.gemspec b/net-ldap.gemspec index 854d4c6e..1414cc8d 100644 --- a/net-ldap.gemspec +++ b/net-ldap.gemspec @@ -24,7 +24,7 @@ Our roadmap for Net::LDAP 1.0 is to gain full client compliance with the most recent LDAP RFCs (4510-4519, plutions of 4520-4532).} s.email = ["blackhedd@rubyforge.org", "gemiel@gmail.com", "rory.ocon@gmail.com", "kaspar.schiess@absurd.li", "austin@rubyforge.org"] s.extra_rdoc_files = ["Manifest.txt", "Contributors.rdoc", "Hacking.rdoc", "History.rdoc", "License.rdoc", "README.rdoc"] - s.files = [".autotest", ".rspec", "Contributors.rdoc", "Hacking.rdoc", "History.rdoc", "License.rdoc", "Manifest.txt", "README.rdoc", "Rakefile", "autotest/discover.rb", "lib/net-ldap.rb", "lib/net/ber.rb", "lib/net/ber/ber_parser.rb", "lib/net/ber/core_ext.rb", "lib/net/ber/core_ext/array.rb", "lib/net/ber/core_ext/bignum.rb", "lib/net/ber/core_ext/false_class.rb", "lib/net/ber/core_ext/fixnum.rb", "lib/net/ber/core_ext/string.rb", "lib/net/ber/core_ext/true_class.rb", "lib/net/ldap.rb", "lib/net/ldap/dataset.rb", "lib/net/ldap/dn.rb", "lib/net/ldap/entry.rb", "lib/net/ldap/filter.rb", "lib/net/ldap/instrumentation.rb", "lib/net/ldap/password.rb", "lib/net/ldap/pdu.rb", "lib/net/snmp.rb", "net-ldap.gemspec", "spec/integration/ssl_ber_spec.rb", "spec/spec.opts", "spec/spec_helper.rb", "spec/unit/ber/ber_spec.rb", "spec/unit/ber/core_ext/string_spec.rb", "spec/unit/ldap/dn_spec.rb", "spec/unit/ldap/entry_spec.rb", "spec/unit/ldap/filter_spec.rb", "spec/unit/ldap_spec.rb", "test/common.rb", "test/test_entry.rb", "test/test_filter.rb", "test/test_ldap_connection.rb", "test/test_ldif.rb", "test/test_password.rb", "test/test_rename.rb", "test/test_snmp.rb", "test/testdata.ldif", "testserver/ldapserver.rb", "testserver/testdata.ldif", "lib/net/ldap/version.rb"] + s.files = `git ls-files`.split $/ s.homepage = %q{http://github.com/ruby-ldap/ruby-net-ldap} s.rdoc_options = ["--main", "README.rdoc"] s.require_paths = ["lib"] diff --git a/spec/spec.opts b/spec/spec.opts deleted file mode 100644 index d019bfbf..00000000 --- a/spec/spec.opts +++ /dev/null @@ -1,2 +0,0 @@ ---format specdoc ---colour \ No newline at end of file diff --git a/spec/unit/ber/ber_spec.rb b/spec/unit/ber/ber_spec.rb index 9806cc85..68242c43 100644 --- a/spec/unit/ber/ber_spec.rb +++ b/spec/unit/ber/ber_spec.rb @@ -90,8 +90,7 @@ raw_string("\x04\x10" + "j1\xB4\xA1*\xA2zA\xAC\xA9`?'\xDDQ\x16") end it "should not fail on strings that can not be converted to UTF-8" do - error = Encoding::UndefinedConversionError - lambda {"\x81".to_ber }.should_not raise_exception(error) + expect { "\x81".to_ber }.not_to raise_error end end end @@ -120,21 +119,21 @@ context "binary data" do let(:data) { ["6a31b4a12aa27a41aca9603f27dd5116"].pack("H*").force_encoding("ASCII-8BIT") } - its(:valid_encoding?) { should be_true } + specify { subject.valid_encoding?.should == true } specify { subject.encoding.name.should == "ASCII-8BIT" } end context "ascii data in UTF-8" do let(:data) { "some text".force_encoding("UTF-8") } - its(:valid_encoding?) { should be_true } + specify { subject.valid_encoding?.should == true } specify { subject.encoding.name.should == "UTF-8" } end context "UTF-8 data in UTF-8" do let(:data) { ["e4b8ad"].pack("H*").force_encoding("UTF-8") } - - its(:valid_encoding?) { should be_true } + + specify { subject.valid_encoding?.should == true } specify { subject.encoding.name.should == "UTF-8" } end end diff --git a/spec/unit/ldap/search_spec.rb b/spec/unit/ldap/search_spec.rb index 1a44f136..55aa94db 100644 --- a/spec/unit/ldap/search_spec.rb +++ b/spec/unit/ldap/search_spec.rb @@ -23,7 +23,7 @@ def search(args) context "when :return_result => false" do it "should return false upon error" do result = @connection.search(:return_result => false) - result.should be_false + result.should == false end end diff --git a/spec/unit/ldap_spec.rb b/spec/unit/ldap_spec.rb index af80f324..8f756b68 100644 --- a/spec/unit/ldap_spec.rb +++ b/spec/unit/ldap_spec.rb @@ -23,10 +23,10 @@ bind_result = flexmock(:bind_result, :success? => true) @connection.should_receive(:bind).with(Hash).and_return(bind_result) - subject.bind.should be_true + subject.bind.should == true payload, result = events.pop - result.should be_true + result.should == true payload[:bind].should == bind_result end @@ -38,7 +38,7 @@ yields(entry = Net::LDAP::Entry.new("uid=user1,ou=users,dc=example,dc=com")). and_return(flexmock(:search_result, :success? => true, :result_code => 0)) - subject.search(:filter => "(uid=user1)").should be_true + subject.search(:filter => "(uid=user1)").should_not be_nil payload, result = events.pop result.should == [entry] diff --git a/test/test_ldif.rb b/test/test_ldif.rb index fb4d5ee9..eb9dfd32 100644 --- a/test/test_ldif.rb +++ b/test/test_ldif.rb @@ -13,6 +13,12 @@ def test_empty_ldif assert_equal(true, ds.empty?) end + def test_ldif_with_version + io = StringIO.new("version: 1") + ds = Net::LDAP::Dataset.read_ldif(io) + assert_equal "1", ds.version + end + def test_ldif_with_comments str = ["# Hello from LDIF-land", "# This is an unterminated comment"] io = StringIO.new(str[0] + "\r\n" + str[1]) @@ -47,6 +53,18 @@ def test_ldif_tab_is_not_continuation assert_equal(true, ds.has_key?("key")) end + def test_ldif_with_base64_dn + str = "dn:: Q049QmFzZTY0IGRuIHRlc3QsT1U9VGVzdCxPVT1Vbml0cyxEQz1leGFtcGxlLERDPWNvbQ==\r\n\r\n" + ds = Net::LDAP::Dataset::read_ldif(StringIO.new(str)) + assert_equal(true, ds.has_key?("CN=Base64 dn test,OU=Test,OU=Units,DC=example,DC=com")) + end + + def test_ldif_with_base64_dn_and_continuation_lines + str = "dn:: Q049QmFzZTY0IGRuIHRlc3Qgd2l0aCBjb250aW51YXRpb24gbGluZSxPVT1UZXN0LE9VPVVua\r\n XRzLERDPWV4YW1wbGUsREM9Y29t\r\n\r\n" + ds = Net::LDAP::Dataset::read_ldif(StringIO.new(str)) + assert_equal(true, ds.has_key?("CN=Base64 dn test with continuation line,OU=Test,OU=Units,DC=example,DC=com")) + end + # TODO, INADEQUATE. We need some more tests # to verify the content. def test_ldif @@ -76,4 +94,11 @@ def test_to_ldif assert_equal(entries.size, ds.size) assert_equal(entries.sort, ds.to_ldif.grep(/^dn:\s*/) { $'.chomp }) end + + def test_to_ldif_with_version + ds = Net::LDAP::Dataset.new + ds.version = "1" + + assert_equal "version: 1", ds.to_ldif_string.chomp + end end