ldap.rb 5.9 KB

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