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