2021-06-01 12:20:20 +00:00
|
|
|
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
|
|
|
|
2017-04-19 10:09:54 +00:00
|
|
|
# Class for establishing LDAP connections. A wrapper around Net::LDAP needed for Auth and Sync.
|
|
|
|
# ATTENTION: Loads custom 'net/ldap/entry' from 'lib/core_ext' which extends the Net::LDAP::Entry class.
|
|
|
|
#
|
|
|
|
# @!attribute [r] connection
|
|
|
|
# @return [Net::LDAP] the Net::LDAP instance with the established connection
|
|
|
|
# @!attribute [r] base_dn
|
|
|
|
# @return [String] the base dn used while initializing the connection
|
|
|
|
class Ldap
|
|
|
|
|
2018-04-12 14:57:37 +00:00
|
|
|
attr_reader :base_dn, :host, :port, :ssl
|
2017-04-19 10:09:54 +00:00
|
|
|
|
|
|
|
# Initializes a LDAP connection.
|
|
|
|
#
|
|
|
|
# @param [Hash] config the configuration for establishing a LDAP connection. Default is Setting 'ldap_config'.
|
|
|
|
# @option config [String] :host_url The LDAP host URL in the format '*protocol*://*host*:*port*'.
|
|
|
|
# @option config [String] :host The LDAP explicit host. May contain the port. Gets overwritten by host_url if given.
|
|
|
|
# @option config [Number] :port The LDAP port. Default is 389 LDAP or 636 for LDAPS. Gets overwritten by host_url if given.
|
|
|
|
# @option config [Boolean] :ssl The LDAP SSL setting. Is set automatically for 'ldaps' protocol. Sets Port to 636 if non other is given.
|
|
|
|
# @option config [String] :base_dn The base DN searches etc. are applied to.
|
|
|
|
# @option config [String] :bind_user The username which should be used for bind.
|
|
|
|
# @option config [String] :bind_pw The password which should be used for bind.
|
|
|
|
#
|
|
|
|
# @example
|
|
|
|
# ldap = Ldap.new
|
|
|
|
#
|
|
|
|
# @return [nil]
|
|
|
|
def initialize(config = nil)
|
|
|
|
@config = config
|
|
|
|
|
|
|
|
if @config.blank?
|
|
|
|
@config = Setting.get('ldap_config')
|
|
|
|
end
|
|
|
|
|
2018-04-12 14:57:37 +00:00
|
|
|
# connect on initialization
|
|
|
|
connection
|
2017-04-19 10:09:54 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# Requests the rootDSE (the root of the directory data tree on a directory server).
|
|
|
|
#
|
|
|
|
# @example
|
|
|
|
# ldap.preferences
|
|
|
|
# #=> [:namingcontexts=>["DC=domain,DC=tld", "CN=Configuration,DC=domain,DC=tld"], :supportedldapversion=>["3", "2"], ...]
|
|
|
|
#
|
|
|
|
# @return [Hash{String => Array<String>}] The found RootDSEs.
|
|
|
|
def preferences
|
2018-04-12 14:57:37 +00:00
|
|
|
connection.search_root_dse.to_h
|
2017-04-19 10:09:54 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# Performs a LDAP search and yields over the found LDAP entries.
|
|
|
|
#
|
|
|
|
# @param filter [String] The filter that should get applied to the search.
|
|
|
|
# @param base [String] The base DN on which the search should get executed. Default is initialization parameter.
|
|
|
|
# @param scope [Net::LDAP::SearchScope] The search scope as defined in Net::LDAP SearchScopes. Default is WholeSubtree.
|
2017-05-05 08:48:40 +00:00
|
|
|
# @param attributes [Array<String>] Limits the requested entry attributes to the given list of attributes which increses the performance.
|
2017-04-19 10:09:54 +00:00
|
|
|
#
|
|
|
|
# @example
|
|
|
|
# ldap.search('(objectClass=group)') do |entry|
|
|
|
|
# p entry
|
|
|
|
# end
|
|
|
|
# #=> <Net::LDAP::Entry...>
|
|
|
|
#
|
|
|
|
# @return [true] Returns always true
|
2020-09-30 09:07:01 +00:00
|
|
|
def search(filter, base: nil, scope: nil, attributes: nil, &block)
|
2017-04-19 10:09:54 +00:00
|
|
|
|
2021-07-16 13:29:38 +00:00
|
|
|
base ||= base_dn
|
2017-04-19 10:09:54 +00:00
|
|
|
scope ||= Net::LDAP::SearchScope_WholeSubtree
|
|
|
|
|
2018-04-12 14:57:37 +00:00
|
|
|
connection.search(
|
2017-04-19 10:09:54 +00:00
|
|
|
base: base,
|
|
|
|
filter: filter,
|
|
|
|
scope: scope,
|
2017-05-05 08:48:40 +00:00
|
|
|
attributes: attributes,
|
2017-04-19 10:09:54 +00:00
|
|
|
return_result: false, # improves performance
|
2020-09-30 09:07:01 +00:00
|
|
|
&block
|
|
|
|
)
|
2017-04-19 10:09:54 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# Checks if there are any entries for the given search criteria.
|
|
|
|
#
|
|
|
|
# @param (see Ldap#search)
|
|
|
|
#
|
|
|
|
# @example
|
|
|
|
# ldap.entries?('(objectClass=group)')
|
|
|
|
# #=> true
|
|
|
|
#
|
|
|
|
# @return [Boolean] Returns true if entries are present false if not.
|
|
|
|
def entries?(*args)
|
2017-05-16 10:13:48 +00:00
|
|
|
found = false
|
|
|
|
search(*args) do |_entry|
|
|
|
|
found = true
|
|
|
|
break
|
|
|
|
end
|
|
|
|
found
|
2017-04-19 10:09:54 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# Counts the entries for the given search criteria.
|
|
|
|
#
|
|
|
|
# @param (see Ldap#search)
|
|
|
|
#
|
|
|
|
# @example
|
|
|
|
# ldap.entries?('(objectClass=group)')
|
|
|
|
# #=> 10
|
|
|
|
#
|
|
|
|
# @return [Number] The count of matching entries.
|
|
|
|
def count(*args)
|
|
|
|
counter = 0
|
|
|
|
search(*args) do |_entry|
|
|
|
|
counter += 1
|
|
|
|
end
|
|
|
|
counter
|
|
|
|
end
|
|
|
|
|
2018-04-12 14:57:37 +00:00
|
|
|
def connection
|
2017-04-19 10:09:54 +00:00
|
|
|
@connection ||= begin
|
|
|
|
attributes_from_config
|
|
|
|
binded_connection
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-04-12 14:57:37 +00:00
|
|
|
private
|
|
|
|
|
2017-04-19 10:09:54 +00:00
|
|
|
def binded_connection
|
|
|
|
# ldap connect
|
|
|
|
ldap = Net::LDAP.new(connection_params)
|
|
|
|
|
|
|
|
# set auth data if needed
|
|
|
|
if @bind_user && @bind_pw
|
|
|
|
ldap.auth @bind_user, @bind_pw
|
|
|
|
end
|
|
|
|
|
|
|
|
return ldap if ldap.bind
|
|
|
|
|
|
|
|
result = ldap.get_operation_result
|
|
|
|
raise Exceptions::UnprocessableEntity, "Can't bind to '#{@host}', #{result.code}, #{result.message}"
|
|
|
|
rescue => e
|
2017-06-01 09:43:19 +00:00
|
|
|
Rails.logger.error e
|
2017-04-19 10:09:54 +00:00
|
|
|
raise Exceptions::UnprocessableEntity, "Can't connect to '#{@host}' on port '#{@port}', #{e}"
|
|
|
|
end
|
|
|
|
|
|
|
|
def connection_params
|
|
|
|
params = {
|
|
|
|
host: @host,
|
|
|
|
port: @port,
|
|
|
|
}
|
|
|
|
|
|
|
|
if @encryption
|
|
|
|
params[:encryption] = @encryption
|
|
|
|
end
|
|
|
|
|
2017-10-27 10:11:50 +00:00
|
|
|
# special workaround for IBM bluepages
|
|
|
|
# see issue #1422 for more details
|
|
|
|
if @host == 'bluepages.ibm.com'
|
|
|
|
params[:force_no_page] = true
|
|
|
|
end
|
|
|
|
|
2017-04-19 10:09:54 +00:00
|
|
|
params
|
|
|
|
end
|
|
|
|
|
|
|
|
def attributes_from_config
|
|
|
|
# might change below
|
|
|
|
@host = @config[:host]
|
|
|
|
@port = @config[:port]
|
|
|
|
@ssl = @config.fetch(:ssl, false)
|
|
|
|
|
|
|
|
parse_host_url
|
|
|
|
parse_host
|
|
|
|
handle_ssl_config
|
|
|
|
handle_bind_crendentials
|
|
|
|
|
|
|
|
@base_dn = @config[:base_dn]
|
|
|
|
|
|
|
|
# fallback to default
|
|
|
|
# port if none given
|
2018-04-12 14:57:37 +00:00
|
|
|
@port ||= 389 # rubocop:disable Naming/MemoizedInstanceVariableName
|
2017-04-19 10:09:54 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def parse_host_url
|
|
|
|
@host_url = @config[:host_url]
|
|
|
|
return if @host_url.blank?
|
|
|
|
raise "Invalid host url '#{@host_url}'" if @host_url !~ %r{\A([^:]+)://(.+?)/?\z}
|
2018-10-09 06:17:41 +00:00
|
|
|
|
2017-04-19 10:09:54 +00:00
|
|
|
@protocol = $1.to_sym
|
|
|
|
@host = $2
|
|
|
|
@ssl = @protocol == :ldaps
|
|
|
|
end
|
|
|
|
|
|
|
|
def parse_host
|
2021-05-12 11:37:44 +00:00
|
|
|
return if @host !~ %r{\A([^:]+):(.+?)\z}
|
2018-10-09 06:17:41 +00:00
|
|
|
|
2017-04-19 10:09:54 +00:00
|
|
|
@host = $1
|
|
|
|
@port = $2.to_i
|
|
|
|
end
|
|
|
|
|
|
|
|
def handle_ssl_config
|
|
|
|
return if !@ssl
|
2018-10-09 06:17:41 +00:00
|
|
|
|
2017-04-19 10:09:54 +00:00
|
|
|
@port ||= @config.fetch(:port, 636)
|
|
|
|
@encryption = {
|
|
|
|
method: :simple_tls,
|
|
|
|
}
|
|
|
|
|
2017-11-23 08:09:44 +00:00
|
|
|
return if @config[:ssl_verify]
|
2018-10-09 06:17:41 +00:00
|
|
|
|
2017-11-23 08:09:44 +00:00
|
|
|
@encryption[:tls_options] = {
|
|
|
|
verify_mode: OpenSSL::SSL::VERIFY_NONE
|
|
|
|
}
|
2017-04-19 10:09:54 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def handle_bind_crendentials
|
|
|
|
@bind_user = @config[:bind_user]
|
|
|
|
@bind_pw = @config[:bind_pw]
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|