lib/net/ldap.rb in net-ldap-0.3.1 vs lib/net/ldap.rb in net-ldap-0.5.1

- old
+ new

@@ -21,10 +21,11 @@ require 'net/ldap/pdu' require 'net/ldap/filter' require 'net/ldap/dataset' require 'net/ldap/password' require 'net/ldap/entry' +require 'net/ldap/version' # == Quick-start for the Impatient # === Quick Example of a user-authentication against an LDAP directory: # # require 'rubygems' @@ -239,20 +240,25 @@ # operation (typically binding first) and then disconnect from the server. # The exception is Net::LDAP#open, which makes a connection to the server # and then keeps it open while it executes a user-supplied block. # Net::LDAP#open closes the connection on completion of the block. class Net::LDAP - VERSION = "0.3.1" class LdapError < StandardError; end SearchScope_BaseObject = 0 SearchScope_SingleLevel = 1 SearchScope_WholeSubtree = 2 SearchScopes = [ SearchScope_BaseObject, SearchScope_SingleLevel, SearchScope_WholeSubtree ] + DerefAliases_Never = 0 + DerefAliases_Search = 1 + DerefAliases_Find = 2 + DerefAliases_Always = 3 + DerefAliasesArray = [ DerefAliases_Never, DerefAliases_Search, DerefAliases_Find, DerefAliases_Always ] + primitive = { 2 => :null } # UnbindRequest body constructed = { 0 => :array, # BindRequest 1 => :array, # BindResponse 2 => :array, # UnbindRequest @@ -306,10 +312,11 @@ DefaultHost = "127.0.0.1" DefaultPort = 389 DefaultAuth = { :method => :anonymous } DefaultTreebase = "dc=com" + DefaultForceNoPage = false StartTlsOid = "1.3.6.1.4.1.1466.20037" ResultStrings = { 0 => "Success", @@ -320,10 +327,11 @@ 10 => "Referral", 12 => "Unavailable crtical extension", 14 => "saslBindInProgress", 16 => "No Such Attribute", 17 => "Undefined Attribute Type", + 19 => "Constraint Violation", 20 => "Attribute or Value Exists", 32 => "No Such Object", 34 => "Invalid DN Syntax", 48 => "Inappropriate Authentication", 49 => "Invalid Credentials", @@ -333,12 +341,15 @@ 53 => "Unwilling to perform", 65 => "Object Class Violation", 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: ResultStrings[code] || "unknown result (#{code})" end @@ -368,20 +379,23 @@ # with the LDAP server. The value is either a Hash containing additional # parameters, or the Symbol :simple_tls, which is equivalent to # specifying the Hash {:method => :simple_tls}. There is a fairly large # range of potential values that may be given for this parameter. See # #encryption for details. + # * :force_no_page => Set to true to prevent paged results even if your + # server says it supports them. This is a fix for MS Active Directory # # Instantiating a Net::LDAP object does <i>not</i> result in network # traffic to the LDAP server. It simply stores the connection and binding # parameters in the object. def initialize(args = {}) @host = args[:host] || DefaultHost @port = args[:port] || DefaultPort @verbose = false # Make this configurable with a switch on the class. @auth = args[:auth] || DefaultAuth @base = args[:base] || DefaultTreebase + @force_no_page = args[:force_no_page] || DefaultForceNoPage encryption args[:encryption] # may be nil if pr = @auth[:password] and pr.respond_to?(:call) @auth[:password] = pr.call end @@ -514,19 +528,21 @@ #-- # Modified the implementation, 20Mar07. We might get a hash of LDAP # response codes instead of a simple numeric code. #++ def get_operation_result + result = @result + result = result.result if result.is_a?(Net::LDAP::PDU) os = OpenStruct.new - if @result.is_a?(Hash) + if result.is_a?(Hash) # We might get a hash of LDAP response codes instead of a simple # numeric code. - os.code = (@result[:resultCode] || "").to_i - os.error_message = @result[:errorMessage] - os.matched_dn = @result[:matchedDN] - elsif @result - os.code = @result + os.code = (result[:resultCode] || "").to_i + os.error_message = result[:errorMessage] + os.matched_dn = result[:matchedDN] + elsif result + os.code = result else os.code = 0 end os.message = Net::LDAP.result2string(os.code) os @@ -580,10 +596,12 @@ # * :scope (one of: Net::LDAP::SearchScope_BaseObject, # Net::LDAP::SearchScope_SingleLevel, # Net::LDAP::SearchScope_WholeSubtree. Default is WholeSubtree.) # * :size (an integer indicating the maximum number of search entries to # return. Default is zero, which signifies no limit.) + # * :deref (one of: Net::LDAP::DerefAliases_Never, Net::LDAP::DerefAliases_Search, + # Net::LDAP::DerefAliases_Find, Net::LDAP::DerefAliases_Always. Default is Never.) # # #search queries the LDAP server and passes <i>each entry</i> to the # caller-supplied block, as an object of type Net::LDAP::Entry. If the # search returns 1000 entries, the block will be called 1000 times. If the # search returns no entries, the block will not be called. @@ -627,15 +645,14 @@ @result = @open_connection.search(args) { |entry| result_set << entry if result_set 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? } end @@ -643,13 +660,13 @@ conn.close if conn end end if return_result_set - @result == 0 ? result_set : nil + (!@result.nil? && @result.result_code == 0) ? result_set : nil else - @result == 0 + @result.success? end end # #bind connects to an LDAP server and requests authentication based on # the <tt>:auth</tt> parameter passed to #open or #new. It takes no @@ -719,11 +736,11 @@ ensure conn.close if conn end end - @result == 0 + @result.success? end # #bind_as is for testing authentication credentials. # # As described under #bind, most LDAP servers require that you supply a @@ -814,18 +831,18 @@ else @result = 0 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.success? end # Modifies the attribute values of a particular entry on the LDAP # directory. Takes a hash with arguments. Supported arguments are: # :dn :: (the full DN of the entry whose attributes are to be modified) @@ -912,18 +929,19 @@ else @result = 0 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.success? end # Add a value to an attribute. Takes the full DN of the entry to modify, # the name (Symbol or String) of the attribute, and the value (String or # Array). If the attribute does not exist (and there are no schema @@ -983,18 +1001,18 @@ else @result = 0 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.success? end alias_method :modify_rdn, :rename # Delete an entry from the LDAP directory. Takes a hash of arguments. The # only supported argument is :dn, which must give the complete DN of the @@ -1011,20 +1029,33 @@ else @result = 0 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.success? 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. #-- # cf. RFC4512 graf 5.1. @@ -1090,12 +1121,16 @@ # Only do this once per Net::LDAP object. # Note, we call a search, and we might be called from inside a search! # MUST refactor the root_dse call out. #++ def paged_searches_supported? + # active directory returns that it supports paged results. However + # it returns binary data in the rfc2696_cookie which throws an + # encoding exception breaking searching. + return false if @force_no_page @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 # This is a private class used internally by the library. It should not # be called by user code. @@ -1235,11 +1270,11 @@ request_pkt = [msgid, request].to_ber_sequence @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" - pdu.result_code + pdu end #-- # Required parameters: :mechanism, :initial_credential and # :challenge_response @@ -1273,11 +1308,11 @@ request = [LdapVersion.to_ber, "".to_ber, sasl].to_ber_appsequence(0) request_pkt = [msgid, request].to_ber_sequence @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) } @@ -1313,11 +1348,40 @@ :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 @@ -1338,10 +1402,16 @@ 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. # @@ -1359,11 +1429,11 @@ # 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_code = 0 + result_pdu = nil n_results = 0 loop { # should collect this into a private helper to clarify the structure query_limit = 0 @@ -1377,32 +1447,37 @@ end request = [ search_base.to_ber, scope.to_ber_enumerated, - 0.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::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)) case pdu.app_tag when 4 # search-data @@ -1415,13 +1490,13 @@ se[:search_referrals] = (pdu.search_referrals || []) yield se 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 return_referrals && pdu.result_code == 10 if block_given? se = Net::LDAP::Entry.new se[:search_referrals] = (pdu.search_referrals || []) yield se end @@ -1441,13 +1516,13 @@ # 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_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 cookie = c.value.read_ber[1] if cookie and cookie.length > 0 @@ -1460,11 +1535,11 @@ end break unless more_pages } # loop - result_code + result_pdu || OpenStruct.new(:status => :failure, :result_code => 1, :message => "Invalid search") end MODIFY_OPERATIONS = { #:nodoc: :add => 0, :delete => 1, @@ -1500,11 +1575,12 @@ ops.to_ber_sequence ].to_ber_appsequence(6) 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 == 7) or raise Net::LDAP::LdapError, "response missing or invalid" - pdu.result_code + + 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 @@ -1521,44 +1597,50 @@ request = [add_dn.to_ber, add_attrs.to_ber_sequence].to_ber_appsequence(8) 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 #-- # TODO: need to support a time limit, in case the server fails to respond. #++ - def rename args + 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 unless new_superior == nil + request << new_superior.to_ber_contextspecific(0) unless new_superior == nil pkt = [next_msgid.to_ber, request.to_ber_appsequence(12)].to_ber_sequence @conn.write pkt (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 #-- # 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].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