src/api/app/models/user_ldap_strategy.rb
# the purpose of this mixin is to get the user functions having to do with ldap into one file
class UserLdapStrategy
@@ldap_search_con = nil
class << self
# This static method tries to find a group with the given group_title to check whether the group is in the LDAP server.
def find_group_with_ldap(group)
result = search_ldap(group)
if result.nil?
Rails.logger.info("Fail to find group: #{group} in LDAP")
false
else
Rails.logger.debug { "group dn: #{result[0]}" }
true
end
end
# This static method performs the search with the given grouplist, user to return the groups that the user in
def render_grouplist_ldap(grouplist, user = nil)
result = []
@@ldap_search_con = initialize_ldap_con(CONFIG['ldap_search_user'], CONFIG['ldap_search_auth']) if @@ldap_search_con.nil?
ldap_con = @@ldap_search_con
if ldap_con.nil?
Rails.logger.info('Unable to connect to LDAP server')
return result
end
if user
# search user
filter = ldap_user_filter(user)
user_dn = ''
user_memberof_attr = ''
ldap_con.search(CONFIG['ldap_search_base'], LDAP::LDAP_SCOPE_SUBTREE, filter) do |entry|
user_dn = entry.dn
user_memberof_attr = entry.vals(CONFIG['ldap_user_memberof_attr']) if CONFIG['ldap_user_memberof_attr'].in?(entry.attrs)
end
if user_dn.empty?
Rails.logger.info("Failed to find #{user} in ldap")
return result
end
Rails.logger.debug { "User dn: #{user_dn} user_memberof_attr: #{user_memberof_attr}" }
end
group_dn = ''
group_member_attr = ''
grouplist.each do |eachgroup|
group = eachgroup if eachgroup.is_a?(String)
group = eachgroup.title if eachgroup.is_a?(Group)
raise ArgumentError, "illegal parameter type to UserLdapStrategy#render_grouplist_ldap?: #{eachgroup.class.name}" unless group.is_a?(String)
# clean group_dn, group_member_attr
group_dn = ''
group_member_attr = ''
filter = ldap_group_filter(group)
Rails.logger.debug { "Search group: #{filter}" }
ldap_con.search(CONFIG['ldap_group_search_base'], LDAP::LDAP_SCOPE_SUBTREE, filter) do |entry|
group_dn = entry.dn
group_member_attr = entry.vals(CONFIG['ldap_group_member_attr']) if CONFIG['ldap_group_member_attr'].in?(entry.attrs)
end
if group_dn.empty?
Rails.logger.info("Failed to find #{group} in ldap")
next
end
if user.nil?
result << eachgroup
next
end
# user memberof attr exist?
if user_memberof_attr && user_memberof_attr.include?(group_dn)
result << eachgroup
Rails.logger.debug { "#{user} is in #{group}" }
next
end
# group member attr exist?
if group_member_attr && group_member_attr.include?(user_dn)
result << eachgroup
Rails.logger.debug { "#{user} is in #{group}" }
next
end
Rails.logger.debug { "#{user} is not in #{group}" }
end
result
end
def authenticate_with_local(password, entry)
if !entry.key?(CONFIG['ldap_auth_attr']) || entry[CONFIG['ldap_auth_attr']].empty?
Rails.logger.info("Failed to get attr:#{CONFIG['ldap_auth_attr']}")
return false
end
ldap_password = entry[CONFIG['ldap_auth_attr']][0]
case CONFIG['ldap_auth_mech']
when :cleartext
ldap_password == password
when :md5
ldap_password == "{MD5}#{Base64.encode64(Digest::MD5.digest(password))}"
else
Rails.logger.error("Unknown ldap_auth_mech setting: #{CONFIG['ldap_auth_mech']}")
false
end
end
# convert distinguished name to user principal name
# see also: http://technet.microsoft.com/en-us/library/cc977992.aspx
def dn2user_principal_name(dn)
upn = ''
# implicitly convert array to string
dn = [dn].flatten.join(',')
begin
dn_components = dn.split(',').map { |n| n.strip.split('=') }
dn_uid = dn_components.select { |x, _| x == 'uid' }.map! { |_, y| y }
dn_path = dn_components.select { |x, _| x == 'dc' }.map! { |_, y| y }
upn = "#{dn_uid.fetch(0)}@#{dn_path.join('.')}"
rescue StandardError
# if we run into unexpected input just return an empty string
end
upn
end
# This static method tries to find a user with the given login and
# password in the active directory server. Returns nil unless
# credentials are correctly found using LDAP.
def find_with_ldap(login, password)
Rails.logger.debug { "Looking for #{login} using ldap" }
# When the server closes the connection, @@ldap_search_con.nil? doesn't catch it
# @@ldap_search_con.bound? doesn't catch it as well. So when an error occurs, we
# simply it try it a seccond time, which forces the ldap connection to
# reinitialize (@@ldap_search_con is unbound and nil).
ldap_first_try = true
user = nil
user_filter = ''
# TODO: This should be refactored
# rubocop:disable Lint/UselessTimes
1.times do
@@ldap_search_con = initialize_ldap_con(CONFIG['ldap_search_user'], CONFIG['ldap_search_auth']) if @@ldap_search_con.nil?
ldap_con = @@ldap_search_con
if ldap_con.nil?
Rails.logger.info('Unable to connect to LDAP server')
return
end
user_filter = ldap_user_filter(login)
Rails.logger.debug { "Search for #{CONFIG['ldap_search_base']} #{user_filter}" }
begin
ldap_con.search(CONFIG['ldap_search_base'], LDAP::LDAP_SCOPE_SUBTREE, user_filter) do |entry|
user = entry.to_hash
end
rescue StandardError
Rails.logger.info("Search failed: error #{@@ldap_search_con.err}: #{@@ldap_search_con.err2string(@@ldap_search_con.err)}")
@@ldap_search_con.unbind
@@ldap_search_con = nil
if ldap_first_try
ldap_first_try = false
redo
end
return
end
end
# rubocop:enable Lint/UselessTimes
if user.nil?
Rails.logger.info('User not found in ldap')
return
end
# Attempt to authenticate user
case CONFIG['ldap_authenticate']
when :local
unless authenticate_with_local(password, user)
Rails.logger.info("Unable to local authenticate #{user['dn']}")
return
end
when :ldap
# ruby-ldap returns true if password is empty
# https://github.com/ruby-ldap/ruby-net-ldap/issues/5
return if password.blank?
# Don't match the passwd locally, try to bind to the ldap server
user_con = initialize_ldap_con(user['dn'], password)
if user_con.nil?
Rails.logger.info("Unable to connect to LDAP server as #{user['dn']} using credentials supplied")
return
else
# Redo the search as the user for situations where the anon search may not be able to see attributes
user_con.search(CONFIG['ldap_search_base'], LDAP::LDAP_SCOPE_SUBTREE, user_filter) do |entry|
user.replace(entry.to_hash)
end
user_con.unbind
end
else # If no CONFIG['ldap_authenticate'] is given do not return the ldap_info !
Rails.logger.error("Unknown ldap_authenticate setting: '#{CONFIG['ldap_authenticate']}' " \
"so #{user['dn']} not authenticated. Ensure ldap_authenticate uses a valid symbol")
return
end
# Only collect the required user information *AFTER* we successfully
# completed the authentication!
ldap_info = []
ldap_info[0] = if user[CONFIG['ldap_mail_attr']]
String.new(user[CONFIG['ldap_mail_attr']][0])
else
dn2user_principal_name(user['dn'])
end
ldap_info[1] = if user[CONFIG['ldap_name_attr']]
String.new(user[CONFIG['ldap_name_attr']][0])
else
login
end
Rails.logger.debug('login success for checking with ldap server')
ldap_info
end
# this method returns a ldap object using the provided user name
# and password
def initialize_ldap_con(user_name, password)
return unless defined?(CONFIG['ldap_servers'])
require 'ldap'
ldap_servers = CONFIG['ldap_servers'].split(':')
# Do 10 attempts to connect to one of the configured LDAP servers. LDAP server
# to connect to is chosen randomly.
(CONFIG['ldap_max_attempts'] || 10).times do
server = ldap_servers[rand(ldap_servers.length)]
conn = try_ldap_con(server, user_name, password)
return conn if conn.try(:bound?)
end
Rails.logger.error("Unable to bind to any LDAP server: #{CONFIG['ldap_servers']}")
nil
end
def try_ldap_con(server, user_name, password)
# implicitly turn array into string
user_name = [user_name].flatten.join
Rails.logger.debug { "Connecting to #{server} as '#{user_name}'" }
port = ldap_port
begin
conn = if CONFIG['ldap_ssl'] == :on || CONFIG['ldap_start_tls'] == :on
LDAP::SSLConn.new(server, port, CONFIG['ldap_start_tls'] == :on)
else
LDAP::Conn.new(server, port)
end
conn.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
conn.set_option(LDAP::LDAP_OPT_REFERRALS, LDAP::LDAP_OPT_OFF) if CONFIG['ldap_referrals'] == :off
conn.bind(user_name, password)
rescue LDAP::ResultError
conn.unbind if conn.try(:bound?)
Rails.logger.info("Not bound as #{user_name}: #{conn.err2string(conn.err)}")
return
end
Rails.logger.debug { "Bound as #{user_name}" }
conn
end
private
# This static method performs the search with the given search_base, filter
def search_ldap(group)
@@ldap_search_con = initialize_ldap_con(CONFIG['ldap_search_user'], CONFIG['ldap_search_auth']) if @@ldap_search_con.nil?
if @@ldap_search_con.nil?
Rails.logger.info('Unable to connect to LDAP server')
return
end
filter = ldap_group_filter(group)
Rails.logger.debug { "Search: #{filter}" }
result = []
@@ldap_search_con.search(CONFIG['ldap_group_search_base'], LDAP::LDAP_SCOPE_SUBTREE, filter) do |entry|
result << entry.dn
result << entry.attrs
end
return if result.empty?
result
end
def ldap_group_filter(group)
if CONFIG.key?('ldap_group_objectclass_attr')
"(&(#{CONFIG['ldap_group_title_attr']}=#{group})(objectclass=#{CONFIG['ldap_group_objectclass_attr']}))"
else
"(#{CONFIG['ldap_group_title_attr']}=#{group})"
end
end
def ldap_user_filter(login)
if CONFIG.key?('ldap_user_filter')
"(&(#{CONFIG['ldap_search_attr']}=#{login})#{CONFIG['ldap_user_filter']})"
else
"(#{CONFIG['ldap_search_attr']}=#{login})"
end
end
def ldap_port
return CONFIG['ldap_port'] if CONFIG['ldap_port']
CONFIG['ldap_ssl'] == :on ? 636 : 389
end
end
def is_in_group?(user, group)
user_in_group_ldap?(user.login, group)
end
def local_role_check(role, object)
local_role_check_with_ldap(role, object)
end
def local_permission_check(roles, object)
groups = object.relationships.groups
local_permission_check_with_ldap(groups.where(role_id: roles))
end
def groups(user)
render_grouplist_ldap(Group.all, user.login)
end
def user_in_group_ldap?(user, group)
group = (group.is_a?(String) ? Group.find_by_title(group) : group)
begin
render_grouplist_ldap([group], user).any?
rescue Exception
Rails.logger.info 'Error occurred in searching user_group in ldap.'
false
end
end
def local_permission_check_with_ldap(group_relationships)
relationship_groups_contains_user?(group_relationships, 'local_permission_check_with_ldap')
end
def local_role_check_with_ldap(role, object)
Rails.logger.debug { "Checking role with ldap: object #{object.name}, role #{role.title}" }
relationship_groups_contains_user?(
object.relationships.groups.where(role_id: role.id).includes(:group), 'local_role_check_with_ldap'
)
end
private
def relationship_groups_contains_user?(relationships, method_name)
relationships.each do |relationship|
return false if relationship.group.nil?
# check whether current user is in this group
return true if user_in_group_ldap?(login, relationship.group)
end
Rails.logger.info "Failed with #{method_name}"
false
end
end