module Devise module LDAP class Connection attr_reader :ldap, :login def initialize(params = {}) # Option scope determines the mapping_file to use @scope = params[:scope] config_file_path = "#{Rails.root}/config/ldap/#{@scope}.yml" # Read the config file depending on the scope ldap_config = YAML.load(ERB.new(File.read(config_file_path)).result) # Alter the ssl option in the configuration ldap_config[Rails.env]["ssl"] = :simple_tls if ldap_config[Rails.env]["ssl"] === true # Determines if logging happens @logging_enabled = ldap_config['logging_enabled'] # Evaluated into Proc objects @auth_username_builder = eval ldap_config['auth_username_builder'] @auth_password_builder = eval ldap_config['auth_password_builder'] # Attributes and groups required for authorisation of an account @group_base = ldap_config["group_base"] @required_groups = ldap_config["required_groups"] @group_membership_attribute = ldap_config.has_key?("group_membership_attribute") ? ldap_config["group_membership_attribute"] : "uniqueMember" @required_attributes = ldap_config["require_attribute"] @required_attributes_presence = ldap_config["require_attribute_presence"] # Various Flags @allow_unauthenticated_bind = ["allow_unauthenticated_bind"] @check_attributes = ldap_config['check_attributes'] @check_attributes_presence = ldap_config['check_attributes_presence'] @use_admin_to_bind = ldap_config['use_admin_to_bind'] @check_group_membership = ldap_config["check_group_membership"] @check_group_membership_without_admin = ldap_config["check_group_membership_without_admin"] @update_passwords = ldap_config['update_passwords'] @create_user = ldap_config['create_user'] # Other params referencing credentails not part of the config file @login = params[:login] @password = params[:password] @new_password = params[:new_password] # Build up the options used to establish an LDAP connection ldap_options = {} ldap_options[:login] = params[:login] if params[:login] ldap_options[:password] = params[:password] if params[:password] ldap_options[:new_password] = params[:new_password] if params[:new_password] ldap_options[:ldap_auth_username_builder] = @auth_username_builder ldap_options[:admin] = @use_admin_to_bind ldap_options[:encryption] = ldap_config[Rails.env]["ssl"].to_sym if ldap_config[Rails.env]["ssl"] # LDAP server to connect to depending on the current Rails environment in use @ldap = Net::LDAP.new(ldap_options) @ldap.host = ldap_config[Rails.env]["host"] @ldap.port = ldap_config[Rails.env]["port"] @ldap.base = ldap_config[Rails.env]["base"] @attribute = ldap_config[Rails.env]["attribute"] # Admin credentials to use if an admin is set to bind to LDAP (Rails environment dependent) @ldap.auth ldap_config[Rails.env]["admin_user"], ldap_config[Rails.env]["admin_password"] if @use_admin_to_bind end # Logs a message to the console and the environment log file def log (message) DeviseLdapMultiple::Logger.send(message) if @logging_enabled end def password_updatable? @update_passwords end def user_creatable? @create_user end # Deletes a parameter from LDAP for an account def delete_param(param) update_ldap [[:delete, param.to_sym, nil]] end def set_param(param, new_value, is_password = false) new_value = @auth_password_builder .call(new_value) if is_password update_ldap( { param.to_sym => new_value } ) end # Returns the DistinguishedName for an account, regardless of if it exists or not (calls a proc to determine the dn if it doesn't exists - proc defined in the config file) def dn @dn ||= begin log("LDAP dn lookup: #{@attribute}=#{@login}") log("LDAP dn lookup asdf: #{@attribute}=#{@login}") ldap_entry = search_for_login if ldap_entry.nil? @auth_username_builder .call(@attribute,@login,@ldap) else ldap_entry.dn end end end def ldap_param_value(param) ldap_entry = search_for_login if ldap_entry unless ldap_entry[param].empty? value = ldap_entry.send(param) log("Requested param #{param} has value #{value}") value else log("Requested param #{param} does not exist") value = nil end else log("Requested ldap entry does not exist") value = nil end end def authenticate! return false unless (@password.present? || @allow_unauthenticated_bind) @ldap.auth(dn, @password) @ldap.bind end def authenticated? authenticate! end def last_message_bad_credentials? @ldap.get_operation_result.error_message.to_s.include? 'AcceptSecurityContext error, data 52e' end def last_message_expired_credentials? @ldap.get_operation_result.error_message.to_s.include? 'AcceptSecurityContext error, data 773' end def authorized? log("Authorizing user #{dn}") if !authenticated? if last_message_bad_credentials? log("Not authorized because of invalid credentials.") elsif last_message_expired_credentials? log("Not authorized because of expired credentials.") else log("Not authorized because not authenticated.") end return false elsif !in_required_groups? log("Not authorized because not in required groups.") return false elsif !has_required_attribute? log("Not authorized because does not have required attribute.") return false elsif !has_required_attribute_presence? log("Not authorized because does not have required attribute present.") return false else return true end end def expired_valid_credentials? log("Authorizing user #{dn}") !authenticated? && last_message_expired_credentials? end def change_password! update_ldap(:userPassword => @auth_password_builder .call(@new_password)) end def in_required_groups? return true unless @check_group_membership || @check_group_membership_without_admin ## FIXME set errors here, the ldap.yml isn't set properly. return false if @required_groups.nil? for group in @required_groups if group.is_a?(Array) return false unless in_group?(group[1], group[0]) else return false unless in_group?(group) end end return true end def in_group?(group_name, group_attribute = LDAP::DEFAULT_GROUP_UNIQUE_MEMBER_LIST_KEY) in_group = false if @check_group_membership_without_admin group_checking_ldap = @ldap else group_checking_ldap = Connection.admin end unless @ldap_check_group_membership group_checking_ldap.search(:base => group_name, :scope => Net::LDAP::SearchScope_BaseObject) do |entry| if entry[group_attribute].include? dn in_group = true log("User #{dn} IS included in group: #{group_name}") end end else # AD optimization - extension will recursively check sub-groups with one query # "(memberof:1.2.840.113556.1.4.1941:=group_name)" search_result = group_checking_ldap.search(:base => dn, :filter => Net::LDAP::Filter.ex("memberof:1.2.840.113556.1.4.1941", group_name), :scope => Net::LDAP::SearchScope_BaseObject) # Will return the user entry if belongs to group otherwise nothing if search_result.length == 1 && search_result[0].dn.eql?(dn) in_group = true log("User #{dn} IS included in group: #{group_name}") end end unless in_group log("User #{dn} is not in group: #{group_name}") end return in_group end def has_required_attribute? return true unless @check_attributes admin_ldap = Connection.admin user = find_ldap_user(admin_ldap) @required_attributes.each do |key,val| matching_attributes = user[key] & Array(val) unless (matching_attributes).any? log("User #{dn} did not match attribute #{key}:#{val}") return false end end return true end def has_required_attribute_presence? return true unless @check_attributes_presence user = search_for_login @required_attributes_presence.each do |key,val| if val && !user.attribute_names.include?(key.to_sym) log("User #{dn} doesn't include attribute #{key}") return false elsif !val && user.attribute_names.include?(key.to_sym) log("User #{dn} includes attribute #{key}") return false end end return true end def user_groups admin_ldap = Connection.admin log("Getting groups for #{dn}") filter = Net::LDAP::Filter.eq(@group_membership_attribute, dn) admin_ldap.search(:filter => filter, :base => @group_base).collect(&:dn) end def valid_login? !search_for_login.nil? end # Searches the LDAP for the login # # @return [Object] the LDAP entry found; nil if not found def search_for_login @login_ldap_entry ||= begin log("LDAP search for login: #{@attribute}=#{@login}") log("Attribute: #{@attribute.to_s}, Login: #{@login.to_s}") log("Scope is: #{@scope}") filter = Net::LDAP::Filter.eq(@attribute.to_s, @login.to_s) ldap_entry = nil match_count = 0 @ldap.search(:filter => filter) { |entry| ldap_entry = entry; match_count+=1} op_result= @ldap.get_operation_result if op_result.code!=0 then log("LDAP Error #{op_result.code}: #{op_result.message}") end log("LDAP search yielded #{match_count} matches") ldap_entry end end private def self.admin ldap = Connection.new(:admin => true).ldap unless ldap.bind log("Cannot bind to admin LDAP user") raise DeviseLdapAuthenticatable::LdapException, "Cannot connect to admin LDAP user" end return ldap end def find_ldap_user(ldap) log("Finding user: #{dn}") ldap.search(:base => dn, :scope => Net::LDAP::SearchScope_BaseObject).try(:first) end def update_ldap(ops) operations = [] if ops.is_a? Hash ops.each do |key,value| operations << [:replace,key,value] end elsif ops.is_a? Array operations = ops end if @use_admin_to_bind privileged_ldap = Connection.admin else authenticate! privileged_ldap = self.ldap end log("Modifying user #{dn}") privileged_ldap.modify(:dn => dn, :operations => operations) end end # class Connection end # module LDAP end # module Devise