ldap.rb 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. # Class for establishing LDAP connections. A wrapper around Net::LDAP needed for Auth and Sync.
  3. # ATTENTION: Loads custom 'net/ldap/entry' from 'lib/core_ext' which extends the Net::LDAP::Entry class.
  4. #
  5. # @!attribute [r] connection
  6. # @return [Net::LDAP] the Net::LDAP instance with the established connection
  7. # @!attribute [r] base_dn
  8. # @return [String] the base dn used while initializing the connection
  9. class Ldap
  10. DEFAULT_PORT = 389
  11. attr_reader :base_dn, :host, :port
  12. # Initializes a LDAP connection.
  13. #
  14. # @param [Hash] config the configuration for establishing a LDAP connection.
  15. # @option config [String] :host The LDAP explicit host. May contain the port.
  16. # @option config [Number] :port The LDAP port. Default is 389 LDAP or 636 for LDAPS. Gets overwritten when it's given inside the host.
  17. # @option config [Boolean] :ssl The LDAP SSL setting. Sets Port to 636 if non other is given.
  18. # @option config [String] :base_dn The base DN searches etc. are applied to.
  19. # @option config [String] :bind_user The username which should be used for bind.
  20. # @option config [String] :bind_pw The password which should be used for bind.
  21. #
  22. # @example
  23. # ldap = Ldap.new
  24. #
  25. # @return [nil]
  26. def initialize(config)
  27. @config = config
  28. # connect on initialization
  29. connection
  30. end
  31. # Requests the rootDSE (the root of the directory data tree on a directory server).
  32. #
  33. # @example
  34. # ldap.preferences
  35. # #=> [:namingcontexts=>["DC=domain,DC=tld", "CN=Configuration,DC=domain,DC=tld"], :supportedldapversion=>["3", "2"], ...]
  36. #
  37. # @return [Hash{String => Array<String>}] The found RootDSEs.
  38. def preferences
  39. connection.search_root_dse.to_h
  40. end
  41. # Performs a LDAP search and yields over the found LDAP entries.
  42. #
  43. # @param filter [String] The filter that should get applied to the search.
  44. # @param base [String] The base DN on which the search should get executed. Default is initialization parameter.
  45. # @param scope [Net::LDAP::SearchScope] The search scope as defined in Net::LDAP SearchScopes. Default is WholeSubtree.
  46. # @param attributes [Array<String>] Limits the requested entry attributes to the given list of attributes which increses the performance.
  47. #
  48. # @example
  49. # ldap.search('(objectClass=group)') do |entry|
  50. # p entry
  51. # end
  52. # #=> <Net::LDAP::Entry...>
  53. #
  54. # @return [true] Returns always true
  55. def search(filter, base: nil, scope: nil, attributes: nil, &)
  56. base ||= base_dn
  57. scope ||= Net::LDAP::SearchScope_WholeSubtree
  58. connection.search(
  59. base: base,
  60. filter: filter,
  61. scope: scope,
  62. attributes: attributes,
  63. return_result: false, # improves performance
  64. &
  65. )
  66. end
  67. # Checks if there are any entries for the given search criteria.
  68. #
  69. # @param (see Ldap#search)
  70. #
  71. # @example
  72. # ldap.entries?('(objectClass=group)')
  73. # #=> true
  74. #
  75. # @return [Boolean] Returns true if entries are present false if not.
  76. def entries?(*)
  77. found = false
  78. search(*) do |_entry|
  79. found = true
  80. break
  81. end
  82. found
  83. end
  84. # Counts the entries for the given search criteria.
  85. #
  86. # @param (see Ldap#search)
  87. #
  88. # @example
  89. # ldap.entries?('(objectClass=group)')
  90. # #=> 10
  91. #
  92. # @return [Number] The count of matching entries.
  93. def count(*)
  94. counter = 0
  95. search(*) do |_entry|
  96. counter += 1
  97. end
  98. counter
  99. end
  100. def connection
  101. @connection ||= begin
  102. attributes_from_config
  103. binded_connection
  104. end
  105. end
  106. private
  107. def binded_connection
  108. # ldap connect
  109. ldap = Net::LDAP.new(connection_params)
  110. # set auth data if needed
  111. if @bind_user && @bind_pw
  112. ldap.auth @bind_user, @bind_pw
  113. end
  114. return ldap if ldap.bind
  115. result = ldap.get_operation_result
  116. raise Exceptions::UnprocessableEntity, "Can't bind to '#{@host}', #{result.code}, #{result.message}"
  117. rescue => e
  118. Rails.logger.error e
  119. raise Exceptions::UnprocessableEntity, "Can't connect to '#{@host}' on port '#{@port}', #{e}"
  120. end
  121. def connection_params
  122. params = {
  123. host: @host,
  124. port: @port,
  125. }
  126. if @encryption
  127. params[:encryption] = @encryption
  128. end
  129. # special workaround for IBM bluepages
  130. # see issue #1422 for more details
  131. if @host == 'bluepages.ibm.com'
  132. params[:force_no_page] = true
  133. end
  134. params
  135. end
  136. def attributes_from_config
  137. # might change below
  138. @host = @config[:host]
  139. @port = @config[:port]
  140. parse_host
  141. handle_ssl_config
  142. handle_bind_crendentials
  143. @base_dn = @config[:base_dn]
  144. # fallback to default
  145. # port if none given
  146. @port ||= DEFAULT_PORT # rubocop:disable Naming/MemoizedInstanceVariableName
  147. end
  148. def parse_host
  149. return if @host !~ %r{\A([^:]+):(.+?)\z}
  150. @host = $1
  151. @port = $2.to_i
  152. end
  153. def handle_ssl_config
  154. return if @config.fetch(:ssl, 'off').eql?('off')
  155. ssl_default_port = DEFAULT_PORT
  156. if @config[:ssl].eql?('ssl')
  157. ssl_default_port = 636
  158. @encryption = {
  159. method: :simple_tls,
  160. }
  161. else
  162. @encryption = {
  163. method: :start_tls,
  164. }
  165. end
  166. @port ||= @config.fetch(:port, ssl_default_port)
  167. if @config[:ssl_verify]
  168. Certificate::ApplySSLCertificates.ensure_fresh_ssl_context
  169. @encryption[:tls_options] = OpenSSL::SSL::SSLContext::DEFAULT_PARAMS
  170. return
  171. end
  172. @encryption[:tls_options] = {
  173. verify_mode: OpenSSL::SSL::VERIFY_NONE
  174. }
  175. end
  176. def handle_bind_crendentials
  177. @bind_user = @config[:bind_user]
  178. @bind_pw = @config[:bind_pw]
  179. end
  180. end