# $Id$ # # Net::LDAP for Ruby # # # # Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved. # # Gmail: garbagecat10 # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # # # == Miscellaneous # # For reasons relating to the source-code layout, this file doesn't # require all the outboard stuff it actually needs, like netber. # Until we figure out how to do that without damaging the directory # structure, we're reliant on user programs to explicitly require # everything, and in the correct order too! # # == BUGS: # # Try querying the objectGUID attribute from an A/D. It's a binary value # which we're reading correctly, but we need to make sure it gets base64-encoded # if we're going to put it out to an LDIF. # #require 'rubygems' #require_gem "eventmachine", ">= 0.3.1" require 'socket' module Net # # class LDAP # class LDAP class LdapError < Exception; end AsnSyntax = { :application => { :constructed => { 0 => :array, # BindRequest 1 => :array, # BindResponse 2 => :array, # UnbindRequest 3 => :array, # SearchRequest 4 => :array, # SearchData 5 => :array, # SearchResult 6 => :array, # ModifyRequest 7 => :array, # ModifyResponse 8 => :array, # AddRequest 9 => :array, # AddResponse 10 => :array, # DelRequest 11 => :array, # DelResponse 12 => :array, # ModifyRdnRequest 13 => :array, # ModifyRdnResponse 14 => :array, # CompareRequest 15 => :array, # CompareResponse 16 => :array, # AbandonRequest } }, :context_specific => { :primitive => { 0 => :string, # password 1 => :string, # Kerberos v4 2 => :string, # Kerberos v5 } } } DefaultHost = "127.0.0.1" DefaultPort = 389 DefaultAuth = {:method => :anonymous} ResultStrings = { 0 => "Success", 1 => "Operations Error", 16 => "No Such Attribute", 17 => "Undefined Attribute Type", 20 => "Attribute or Value Exists", 32 => "No Such Object", 34 => "Invalid DN Syntax", 48 => "Invalid DN Syntax", 48 => "Inappropriate Authentication", 49 => "Invalid Credentials", 50 => "Insufficient Access Rights", 51 => "Busy", 52 => "Unavailable", 53 => "Unwilling to perform", 68 => "Entry Already Exists" } # # LDAP::result2string # def LDAP::result2string code ResultStrings[code] || "unknown result (#{code})" end # # initialize # 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 # This variable is only set when we are created with LDAP::open. # All of our internal methods will connect using it, or else # they will create their own. @open_connection = nil end # # open # def LDAP::open end # # search # def search args conn = Connection.new( :host => @host, :port => @port ) # TODO, hardcoded Ldap result code in next line (rc = conn.bind @auth) == 0 or return rc result_code = conn.search( args ) {|values| block_given? and yield( values ) } result_code end # # bind # Bind and unbind. # Can serve as a connectivity test as well as an auth test. # def bind conn = Connection.new( :host => @host, :port => @port ) conn.bind @auth end # # bind_as # This is for testing authentication credentials. # Most likely a "standard" name (like a CN or an email # address) will be presented along with a password. # We'll bind with the main credential given in the # constructor, query the full DN of the user given # to us as a parameter, then unbind and rebind as the # new user. # def bind_as end # # add # Add a full RDN to the remote DIS. # def add args conn = Connection.new( :host => @host, :port => @port ) # TODO, hardcoded Ldap result code in next line (rc = conn.bind @auth) == 0 or return rc conn.add( args ) end # # modify # Modify the attributes of an entry on the remote DIS. # def modify args conn = Connection.new( :host => @host, :port => @port ) # TODO, hardcoded Ldap result code in next line (rc = conn.bind @auth) == 0 or return rc conn.modify( args ) end # # rename # Rename an entry on the remote DIS by changing the last RDN of its DN. # def rename args conn = Connection.new( :host => @host, :port => @port ) # TODO, hardcoded Ldap result code in next line (rc = conn.bind @auth) == 0 or return rc conn.rename( args ) end end # class LDAP class LDAP class Connection LdapVersion = 3 # # initialize # def initialize server begin @conn = TCPsocket.new( server[:host], server[:port] ) rescue raise LdapError.new( "no connection to server" ) end block_given? and yield self end # # next_msgid # def next_msgid @msgid ||= 0 @msgid += 1 end # # bind # def bind auth user,psw = case auth[:method] when :anonymous ["",""] when :simple [auth[:username] || auth[:dn], auth[:password]] end raise LdapError.new( "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 @conn.write request_pkt (be = @conn.read_ber(AsnSyntax) and pdu = Net::LdapPdu.new( be )) or raise LdapError.new( "no bind result" ) pdu.result_code end # # search # 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]) || Filter.eq( "objectclass", "*" ) search_base = (args && args[:base]) || "dc=example,dc=com" search_attributes = ((args && args[:attributes]) || []).map {|attr| attr.to_s.to_ber} request = [ search_base.to_ber, 2.to_ber_enumerated, 0.to_ber_enumerated, 0.to_ber, 0.to_ber, false.to_ber, search_filter.to_ber, search_attributes.to_ber_sequence ].to_ber_appsequence(3) pkt = [next_msgid.to_ber, request].to_ber_sequence @conn.write pkt search_results = {} result_code = 0 while (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) case pdu.app_tag when 4 # search-data search_results [pdu.search_dn] = pdu.search_attributes when 5 # search-result result_code = pdu.result_code block_given? and yield( search_results ) break else raise LdapError.new( "invalid response-type in search: #{pdu.app_tag}" ) end end result_code end # # modify # 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" modify_ops = [] a = args[:operations] and a.each {|op, attr, values| # TODO, fix the following line, which gives a bogus error # if the opcode is invalid. op_1 = {:add => 0, :delete => 1, :replace => 2} [op.to_sym].to_ber_enumerated modify_ops << [op_1, [attr.to_s.to_ber, values.to_a.map {|v| v.to_ber}.to_ber_set].to_ber_sequence].to_ber_sequence } request = [modify_dn.to_ber, modify_ops.to_ber_sequence].to_ber_appsequence(6) pkt = [next_msgid.to_ber, request].to_ber_sequence @conn.write pkt (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 7) or raise LdapError.new( "response missing or invalid" ) pdu.result_code end # # add # TODO, need to support a time limit, in case the server fails to respond. # def add args add_dn = args[:dn] or raise LdapError.new("Unable to add empty DN") add_attrs = [] a = args[:attributes] and a.each {|k,v| add_attrs << [ k.to_s.to_ber, v.to_a.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 @conn.write pkt (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 9) or raise LdapError.new( "response missing or invalid" ) pdu.result_code end # # rename # 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 request = [old_dn.to_ber, new_rdn.to_ber, delete_attrs.to_ber].to_ber_appsequence(12) pkt = [next_msgid.to_ber, request].to_ber_sequence @conn.write pkt (be = @conn.read_ber(AsnSyntax)) && (pdu = LdapPdu.new( be )) && (pdu.app_tag == 13) or raise LdapError.new( "response missing or invalid" ) pdu.result_code end end # class Connection end # class LDAP end # module Net #------------------------------------------------------ if __FILE__ == $0 puts "No default action" end