#this code boughts pieces from activeldap and net-ldap require 'rack' require 'net/ldap' require 'net/ntlm' require 'uri' module OmniAuth module Strategies class LDAP class Adaptor class LdapError < StandardError; end class ConfigurationError < StandardError; end class AuthenticationError < StandardError; end class ConnectionError < StandardError; end VALID_ADAPTER_CONFIGURATION_KEYS = [:host, :port, :method, :bind_dn, :password, :try_sasl, :sasl_mechanisms, :uid, :base] MUST_HAVE_KEYS = [:host, :port, :method, :uid, :base] METHOD = { :ssl => :simple_tls, :tls => :start_tls, :plain => nil } attr_accessor :bind_dn, :password attr_reader :connection, :uid, :base def initialize(configuration={}) @connection = nil @disconnected = false @bound = false @configuration = configuration.dup @logger = @configuration.delete(:logger) message = [] MUST_HAVE_KEYS.each do |name| message << name if configuration[name].nil? end raise ArgumentError.new(message.join(",") +" MUST be provided") unless message.empty? VALID_ADAPTER_CONFIGURATION_KEYS.each do |name| instance_variable_set("@#{name}", configuration[name]) end end def connect(options={}) host = options[:host] || @host method = options[:method] || @method || :plain port = options[:port] || @port || ensure_port(method) method = ensure_method(method) @disconnected = false @bound = false @bind_tried = false config = { :host => host, :port => port, } config[:encryption] = {:method => method} if method @connection, @uri, @with_start_tls = begin uri = construct_uri(host, port, method == :simple_tls) with_start_tls = method == :start_tls puts ({:uri => uri, :with_start_tls => with_start_tls}).inspect [Net::LDAP::Connection.new(config), uri, with_start_tls] rescue Net::LDAP::LdapError raise ConnectionError, $!.message end end def unbind(options={}) @connection.close # Net::LDAP doesn't implement unbind. end def bind(options={}) connect(options) unless connecting? begin @bind_tried = true bind_dn = (options[:bind_dn] || @bind_dn).to_s try_sasl = options.has_key?(:try_sasl) ? options[:try_sasl] : @try_sasl # Rough bind loop: # Attempt 1: SASL if available # Attempt 2: SIMPLE with credentials if password block if try_sasl and sasl_bind(bind_dn, options) puts "bind with sasl" elsif simple_bind(bind_dn, options) puts "bind with simple" else message = yield if block_given? message ||= ('All authentication methods for %s exhausted.') % target raise AuthenticationError, message end @bound = true rescue Net::LDAP::LdapError raise AuthenticationError, $!.message end end def disconnect!(options={}) unbind(options) @connection = @uri = @with_start_tls = nil @disconnected = true end def rebind(options={}) unbind(options) if bound? connect(options) end def connecting? !@connection.nil? and !@disconnected end def bound? connecting? and @bound end def search(options={}, &block) base = options[:base] filter = options[:filter] limit = options[:limit] args = { :base => @base, :filter => filter, :size => limit } puts args.inspect attributes = {} execute(:search, args) do |entry| entry.attribute_names.each do |name| attributes[name] = entry[name] end end attributes end private def execute(method, *args, &block) result = @connection.send(method, *args, &block) message = nil if result.is_a?(Hash) message = result[:errorMessage] result = result[:resultCode] end unless result.zero? message = [Net::LDAP.result2string(result), message].compact.join(": ") raise LdapError, message end end def ensure_port(method) if method == :ssl URI::LDAPS::DEFAULT_PORT else URI::LDAP::DEFAULT_PORT end end def prepare_connection(options) end def ensure_method(method) method ||= "plain" normalized_method = method.to_s.downcase.to_sym return METHOD[normalized_method] if METHOD.has_key?(normalized_method) available_methods = METHOD.keys.collect {|m| m.inspect}.join(", ") format = "%s is not one of the available connect methods: %s" raise ConfigurationError, format % [method.inspect, available_methods] end def sasl_bind(bind_dn, options={}) sasl_mechanisms = options[:sasl_mechanisms] || @sasl_mechanisms sasl_mechanisms.each do |mechanism| begin normalized_mechanism = mechanism.downcase.gsub(/-/, '_') sasl_bind_setup = "sasl_bind_setup_#{normalized_mechanism}" next unless respond_to?(sasl_bind_setup, true) initial_credential, challenge_response = send(sasl_bind_setup, bind_dn, options) args = { :method => :sasl, :initial_credential => initial_credential, :mechanism => mechanism, :challenge_response => challenge_response, } info = { :name => "bind: SASL", :dn => bind_dn, :mechanism => mechanism, } puts info.inspect execute(:bind, args) return true rescue Exception => e puts e.message end end false end def sasl_bind_setup_digest_md5(bind_dn, options) initial_credential = "" challenge_response = Proc.new do |cred| pref = SASL::Preferences.new :digest_uri => "ldap/#{@host}", :username => bind_dn, :has_password? => true, :password => options[:password]||@password sasl = SASL.new("DIGEST-MD5", pref) response = sasl.receive("challenge", cred) response[1] end [initial_credential, challenge_response] end def sasl_bind_setup_gss_spnego(bind_dn, options) puts options.inspect user,psw = [bind_dn, options[:password]||@password] raise LdapError.new( "invalid binding information" ) unless (user && psw) nego = proc {|challenge| t2_msg = Net::NTLM::Message.parse( challenge ) user, domain = user.split('\\').reverse t2_msg.target_name = Net::NTLM::encode_utf16le(domain) if domain t3_msg = t2_msg.response( {:user => user, :password => psw}, {:ntlmv2 => true} ) t3_msg.serialize } [Net::NTLM::Message::Type1.new.serialize, nego] end def simple_bind(bind_dn, options={}) args = { :method => :simple, :username => bind_dn, :password => options[:password]||@password, } execute(:bind, args) true end def construct_uri(host, port, ssl) protocol = ssl ? "ldaps" : "ldap" URI.parse("#{protocol}://#{host}:#{port}").to_s end def target return nil if @uri.nil? if @with_start_tls "#{@uri}(StartTLS)" else @uri end end end end end end