require 'net-ldap' module EgovUtils class AuthSourceException < Exception; end class AuthSourceTimeoutException < AuthSourceException; end class AuthSource NETWORK_EXCEPTIONS = [ Net::LDAP::LdapError, Errno::ECONNABORTED, Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EHOSTDOWN, Errno::EHOSTUNREACH, SocketError ] def self.config YAML.load_file(Rails.root.join('config', 'config.yml'))['ldap'] end def self.providers config.keys end def self.authenticate(login, password) providers.collect{|p| AuthSource.new(p).authenticate(login, password) }.compact.first end def self.kerberos_providers config.select{|provider, config| config['kerberos']}.keys end def self.find_kerberos_user(login) kerberos_providers.collect{|p| AuthSource.new(p).get_kerberos_user_dn(login) }.compact.first end attr_accessor :provider def initialize(provider) require 'net-ldap' @provider = provider raise "EgovUtils::AuthSource#initialize - Non existing provider (#{provider.to_s})" unless self.class.providers.include?(provider) end def options @options ||= self.class.config[provider].dup end # Resolves host name - it is used only if option :resolve_host is set to true and expect :domain to be defined as well. # ldap controller host is resolved by asking for the _ldap._tcp. DNS record and takes first - solve the load balancing of ldap queries. def host_dns require 'resolv' @host_dns = Resolv::DNS.open do |dns| dns.getresource('_ldap._tcp.'+options['domain'], Resolv::DNS::Resource::IN::SRV) end end # Get host of ldap controller from options. # # * :host - this just give one host and EgovUtils just asks that host # * :domain with :resolve_host set to true. Domain should be domain for your ldap users. # in this configuration ldap controller host is resolved by asking for the _ldap._tcp. DNS record and takes first - solve the load balancing of ldap queries. def host if options['host'] options['host'] elsif options['resolve_host'] && options['domain'] host_dns.target.to_s end end # Returns ldap controller port. If :resolve_host is set to true and option for port is not defined, it uses port from DNS response. def port options['resolve_host'] ? (options['port'] || host_dns.port.to_i) : options['port'] end def encryption case options['method'].to_s when 'ssl' :simple_tls when 'tls' :start_tls else nil end end def authenticate(login, password) return nil if login.blank? || password.blank? with_timeout do attrs = get_user_dn(login, password) if attrs && attrs[:dn] && authenticate_dn(attrs[:dn], password) Rails.logger.info "AuthSource#authenticate successful for '#{login}' on provider #{provider} - user found" if Rails.logger return attrs.except(:dn) end end rescue *NETWORK_EXCEPTIONS => e raise AuthSourceException.new(e.message) end def get_kerberos_user_dn(login) return nil if login.blank? with_timeout do search_user_dn(login) end rescue *NETWORK_EXCEPTIONS => e raise AuthSourceException.new(e.message) end def base_user_filter Net::LDAP::Filter.eq("objectClass", "user") & Net::LDAP::Filter.eq("objectCategory", "person") end def base_group_filter options['active_directory'] ? Net::LDAP::Filter.eq("objectClass", "group") : Net::LDAP::Filter.eq('objectClass', 'groupOfNames') end # Check if a DN (user record) authenticates with the password def authenticate_dn(dn, password) if dn.present? && password.present? initialize_ldap_con(dn, password).bind end end # Searches the source for users and returns an array of results def search_user(q, by_login=false) q = q.to_s.strip return [] unless q.present? results = [] search_filter = base_user_filter & user_search_filters(q) ldap_con = initialize_ldap_con(options['bind_dn'], options['password']) ldap_con.search(:base => options['base'], :filter => search_filter, :attributes => user_search_attributes, :size => 10) do |entry| attrs = get_user_attributes_from_ldap_entry(entry) if attrs attrs[:login] = get_attr(entry, options['attributes']['username']) results << attrs end end results rescue *NETWORK_EXCEPTIONS => e raise AuthSourceException.new(e.message) end def search_group(q, by_login=false) q = q.to_s.strip return [] unless q.present? results = [] search_filter = base_group_filter & group_search_filters(q) ldap_con = initialize_ldap_con(options['bind_dn'], options['password']) ldap_con.search(:base => options['base'], :filter => search_filter, :attributes => group_search_attributes, :size => 10) do |entry| attrs = get_group_attributes_from_ldap_entry(entry) results << attrs if attrs end results rescue *NETWORK_EXCEPTIONS => e raise AuthSourceException.new(e.message) end def member?(user_dn, group_dn) ldap_con = initialize_ldap_con(options['bind_dn'], options['password']) Rails.logger.debug("Membership in group (#{group_dn}) for (#{user_dn})") ldap_con.search(base: user_dn, filter: base_user_filter & Net::LDAP::Filter.ex('memberOf:1.2.840.113556.1.4.1941', group_dn), attributes: ['dn']) do |entry| return true end return false end def group_members(group_dn) ldap_con = initialize_ldap_con(options['bind_dn'], options['password']) results = [] if group_dn ldap_con.search(base: options['base'], filter: base_user_filter & Net::LDAP::Filter.ex('memberOf:1.2.840.113556.1.4.1941', group_dn), attributes: user_search_attributes) do |entry| attrs = get_user_attributes_from_ldap_entry(entry) if attrs attrs[:login] = get_attr(entry, options['attributes']['username']) results << attrs end end end results end def onthefly_register? !!options['onthefly_register'] end def register_members_only? options['onthefly_register'] == 'members' end private def with_timeout(&block) timeout = 20 Timeout.timeout(timeout) do return yield end rescue Timeout::Error => e raise AuthSourceTimeoutException.new(e.message) end def initialize_ldap_con(ldap_user, ldap_password) options = { :host => self.host, :port => self.port, :encryption => encryption } unless ldap_user.blank? && ldap_password.blank? options.merge!(:auth => { :method => :simple, :username => ldap_user, :password => ldap_password }) else options.merge!(:auth => { :method => :anonymous }) end Net::LDAP.new options end def get_user_attributes_from_ldap_entry(entry) { :dn => entry.dn, :login => get_attr(entry, options['attributes']['username']), :firstname => get_attr(entry, options['attributes']['first_name']), :lastname => get_attr(entry, options['attributes']['last_name']), :mail => get_attr(entry, options['attributes']['email']), :provider => provider } end def get_group_attributes_from_ldap_entry(entry) { :dn => entry.dn, :name => get_attr(entry, 'cn'), :provider => provider, :ldap_uid => get_sid_string( get_attr(entry, 'objectSID') ), :external_uid => entry.dn } end # Return the attributes needed for the LDAP search. It will only # include the user attributes if on-the-fly registration is enabled def user_search_attributes ['dn'] + options['attributes']['username'] + options['attributes']['email'] + [options['attributes']['name'], options['attributes']['first_name'], options['attributes']['last_name']] end def login_attributes if onthefly_register? user_search_attributes else ['dn'] end end def group_search_attributes ['dn', 'cn', 'objectSID'] end def get_user_dn(login, password=nil) ldap_con = nil if options['bind_dn'].include?("$login") ldap_con = initialize_ldap_con(options['bind_dn'].sub("$login", Net::LDAP::DN.escape(login)), password) else ldap_con = initialize_ldap_con(options['bind_dn'], options['password']) end attrs = nil search_filter = base_user_filter & login_filters(login) ldap_con.search( :base => options['base'], :filter => search_filter, :attributes=> user_search_attributes) do |entry| if onthefly_register? attrs = get_user_attributes_from_ldap_entry(entry) else attrs = {:dn => entry.dn} end Rails.logger.debug "DN found for #{login}: #{attrs[:dn]}" if Rails.logger && Rails.logger.debug? end attrs end def get_group_dn(**options) ldap_con = initialize_ldap_con(options['bind_dn'], options['password']) ldap_con.search(base: options['base'], filter: base_group_filter & ( options[:sid] ? Net::LDAP::Filter.eq('objectSID', options[:sid]) : group_search_filters(options[:name]) ), attributes: ['dn']) do |entry| return get_attr(entry, 'dn') end end def search_user_dn(login, password=nil) ldap_con = nil if options['bind_dn'].include?("$login") ldap_con = initialize_ldap_con(options['bind_dn'].sub("$login", Net::LDAP::DN.escape(login)), password) else ldap_con = initialize_ldap_con(options['bind_dn'], options['password']) end attrs = nil search_filter = login_search_filters(login) #base_filter & Net::LDAP::Filter.eq(self.attr_login, login) ldap_con.search( :base => options['base'], :filter => search_filter, :attributes=> user_search_attributes) do |entry| attrs ||= get_user_attributes_from_ldap_entry(entry) Rails.logger.debug "DN found for #{login}: #{attrs[:dn]}" if Rails.logger && Rails.logger.debug? end attrs end def login_filters(login) filters = options['attributes']['username'].collect{|un| Net::LDAP::Filter.eq(un, login)} filters[1..-1].inject(filters.first){|filter, lf| filter | lf } end def login_search_filters(q) filters = options['attributes']['username'].collect{|un| Net::LDAP::Filter.begins(un, q)} filters[1..-1].inject(filters.first){|filter, lf| filter | lf } end def user_search_filters(q) Net::LDAP::Filter.begins(options['attributes']['name'], q) | Net::LDAP::Filter.begins(options['attributes']['first_name'], q) | Net::LDAP::Filter.begins(options['attributes']['last_name'], q) | Net::LDAP::Filter.begins(options['attributes']['username'].first, q) | Net::LDAP::Filter.begins(options['attributes']['email'].first, q) end def group_search_filters(q) Net::LDAP::Filter.begins('cn', q) end def get_attr(entry, attr_name) if attr_name.is_a? Array attr_name.collect{|an| get_attr(entry, an).presence }.compact.first.to_s elsif !attr_name.blank? value = entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name] value.to_s.force_encoding('UTF-8') end end # converts hex representation of SID returned by AD to its string representation def get_sid_string(data) return if data.nil? sid = data.unpack('b x nN V*') sid[1, 2] = Array[nil, b48_to_fixnum(sid[1], sid[2])] 'S-' + sid.compact.join('-') end B32 = 2**32 def b48_to_fixnum(i16, i32) i32 + (i16 * B32) end end end