123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212 |
- # Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
- # 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
- attr_reader :base_dn, :host, :port, :ssl
- # Initializes a LDAP connection.
- #
- # @param [Hash] config the configuration for establishing a LDAP connection.
- # @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)
- @config = config
- # connect on initialization
- connection
- 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
- connection.search_root_dse.to_h
- 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.
- # @param attributes [Array<String>] Limits the requested entry attributes to the given list of attributes which increses the performance.
- #
- # @example
- # ldap.search('(objectClass=group)') do |entry|
- # p entry
- # end
- # #=> <Net::LDAP::Entry...>
- #
- # @return [true] Returns always true
- def search(filter, base: nil, scope: nil, attributes: nil, &block)
- base ||= base_dn
- scope ||= Net::LDAP::SearchScope_WholeSubtree
- connection.search(
- base: base,
- filter: filter,
- scope: scope,
- attributes: attributes,
- return_result: false, # improves performance
- &block
- )
- 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)
- found = false
- search(*args) do |_entry|
- found = true
- break
- end
- found
- 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
- def connection
- @connection ||= begin
- attributes_from_config
- binded_connection
- end
- end
- private
- 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
- Rails.logger.error e
- 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
- # special workaround for IBM bluepages
- # see issue #1422 for more details
- if @host == 'bluepages.ibm.com'
- params[:force_no_page] = true
- end
- 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
- @port ||= 389 # rubocop:disable Naming/MemoizedInstanceVariableName
- 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}
- @protocol = $1.to_sym
- @host = $2
- @ssl = @protocol == :ldaps
- end
- def parse_host
- return if @host !~ %r{\A([^:]+):(.+?)\z}
- @host = $1
- @port = $2.to_i
- end
- def handle_ssl_config
- return if !@ssl
- @port ||= @config.fetch(:port, 636)
- @encryption = {
- method: :simple_tls,
- }
- return if @config[:ssl_verify]
- @encryption[:tls_options] = {
- verify_mode: OpenSSL::SSL::VERIFY_NONE
- }
- end
- def handle_bind_crendentials
- @bind_user = @config[:bind_user]
- @bind_pw = @config[:bind_pw]
- end
- end
|