150 lines
4.3 KiB
Ruby
150 lines
4.3 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Gitlab
|
|
module Auth
|
|
module Ldap
|
|
class Adapter
|
|
SEARCH_RETRY_FACTOR = [1, 1, 2, 3].freeze
|
|
MAX_SEARCH_RETRIES = Rails.env.test? ? 1 : SEARCH_RETRY_FACTOR.size
|
|
|
|
attr_reader :provider, :ldap
|
|
|
|
def self.open(provider, &block)
|
|
Net::LDAP.open(config(provider).adapter_options) do |ldap|
|
|
block.call(self.new(provider, ldap))
|
|
end
|
|
end
|
|
|
|
def self.config(provider)
|
|
Gitlab::Auth::Ldap::Config.new(provider)
|
|
end
|
|
|
|
def initialize(provider, ldap = nil)
|
|
@provider = provider
|
|
@ldap = ldap || renew_connection_adapter
|
|
end
|
|
|
|
def config
|
|
Gitlab::Auth::Ldap::Config.new(provider)
|
|
end
|
|
|
|
def users(fields, value, limit = nil)
|
|
options = user_options(Array(fields), value, limit)
|
|
users_search(options)
|
|
end
|
|
|
|
def user(*args)
|
|
users(*args).first
|
|
end
|
|
|
|
def dn_matches_filter?(dn, filter)
|
|
ldap_search(base: dn,
|
|
filter: filter,
|
|
scope: Net::LDAP::SearchScope_BaseObject,
|
|
attributes: %w{dn}).any?
|
|
end
|
|
|
|
def ldap_search(*args)
|
|
retries ||= 0
|
|
|
|
# Net::LDAP's `time` argument doesn't work. Use Ruby `Timeout` instead.
|
|
Timeout.timeout(timeout_time(retries)) do
|
|
results = ldap.search(*args)
|
|
|
|
if results.nil?
|
|
response = ldap.get_operation_result
|
|
check_empty_response_code(response)
|
|
[]
|
|
else
|
|
results
|
|
end
|
|
end
|
|
rescue Net::LDAP::Error, Timeout::Error => error
|
|
retries += 1
|
|
error_message = connection_error_message(error)
|
|
|
|
Gitlab::AppLogger.warn(error_message)
|
|
|
|
if retries < MAX_SEARCH_RETRIES
|
|
renew_connection_adapter
|
|
retry
|
|
else
|
|
raise LdapConnectionError, error_message
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def timeout_time(retry_number)
|
|
SEARCH_RETRY_FACTOR[retry_number] * config.timeout
|
|
end
|
|
|
|
def users_search(options)
|
|
entries = ldap_search(options).select do |entry|
|
|
entry.respond_to? config.uid
|
|
end
|
|
|
|
entries.map do |entry|
|
|
Gitlab::Auth::Ldap::Person.new(entry, provider)
|
|
end
|
|
end
|
|
|
|
def user_options(fields, value, limit)
|
|
options = {
|
|
attributes: Gitlab::Auth::Ldap::Person.ldap_attributes(config),
|
|
base: config.base
|
|
}
|
|
|
|
options[:size] = limit if limit
|
|
|
|
if fields.include?('dn')
|
|
raise ArgumentError, 'It is not currently possible to search the DN and other fields at the same time.' if fields.size > 1
|
|
|
|
options[:base] = value
|
|
options[:scope] = Net::LDAP::SearchScope_BaseObject
|
|
else
|
|
filter = fields.map { |field| Net::LDAP::Filter.eq(field, value) }.inject(:|)
|
|
end
|
|
|
|
options.merge(filter: user_filter(filter))
|
|
end
|
|
|
|
def user_filter(filter = nil)
|
|
user_filter = config.constructed_user_filter if config.user_filter.present?
|
|
|
|
if user_filter && filter
|
|
Net::LDAP::Filter.join(filter, user_filter)
|
|
elsif user_filter
|
|
user_filter
|
|
else
|
|
filter
|
|
end
|
|
end
|
|
|
|
def connection_error_message(exception)
|
|
if exception.is_a?(Timeout::Error)
|
|
"LDAP search timed out after #{config.timeout} seconds"
|
|
else
|
|
"LDAP search raised exception #{exception.class}: #{exception.message}"
|
|
end
|
|
end
|
|
|
|
def renew_connection_adapter
|
|
@ldap = Net::LDAP.new(config.adapter_options)
|
|
end
|
|
|
|
def check_empty_response_code(response)
|
|
if config.retry_empty_result_with_codes.include?(response.code)
|
|
raise Net::LDAP::Error, "Got empty results with response code: #{response.code}, message: #{response.message}"
|
|
end
|
|
|
|
unless response.code == 0
|
|
Gitlab::AppLogger.warn("LDAP search error: #{response.message}")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
Gitlab::Auth::Ldap::Adapter.prepend_mod_with('Gitlab::Auth::Ldap::Adapter')
|