You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

auth_source_ldap.rb 7.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. # frozen_string_literal: true
  2. # Redmine - project management software
  3. # Copyright (C) 2006-2019 Jean-Philippe Lang
  4. #
  5. # This program is free software; you can redistribute it and/or
  6. # modify it under the terms of the GNU General Public License
  7. # as published by the Free Software Foundation; either version 2
  8. # of the License, or (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program; if not, write to the Free Software
  17. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. require 'net/ldap'
  19. require 'net/ldap/dn'
  20. require 'timeout'
  21. class AuthSourceLdap < AuthSource
  22. NETWORK_EXCEPTIONS = [
  23. Net::LDAP::Error,
  24. Errno::ECONNABORTED, Errno::ECONNREFUSED, Errno::ECONNRESET,
  25. Errno::EHOSTDOWN, Errno::EHOSTUNREACH,
  26. SocketError
  27. ]
  28. validates_presence_of :host, :port, :attr_login
  29. validates_length_of :name, :host, :maximum => 60, :allow_nil => true
  30. validates_length_of :account, :account_password, :base_dn, :maximum => 255, :allow_blank => true
  31. validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30, :allow_nil => true
  32. validates_numericality_of :port, :only_integer => true
  33. validates_numericality_of :timeout, :only_integer => true, :allow_blank => true
  34. validate :validate_filter
  35. before_validation :strip_ldap_attributes
  36. safe_attributes 'ldap_mode'
  37. LDAP_MODES = [
  38. :ldap,
  39. :ldaps_verify_none,
  40. :ldaps_verify_peer
  41. ]
  42. def initialize(attributes=nil, *args)
  43. super
  44. self.port = 389 if self.port == 0
  45. end
  46. def authenticate(login, password)
  47. return nil if login.blank? || password.blank?
  48. with_timeout do
  49. attrs = get_user_dn(login, password)
  50. if attrs && attrs[:dn] && authenticate_dn(attrs[:dn], password)
  51. logger.debug "Authentication successful for '#{login}'" if logger && logger.debug?
  52. return attrs.except(:dn)
  53. end
  54. end
  55. rescue *NETWORK_EXCEPTIONS => e
  56. raise AuthSourceException.new(e.message)
  57. end
  58. # Test the connection to the LDAP
  59. def test_connection
  60. with_timeout do
  61. ldap_con = initialize_ldap_con(self.account, self.account_password)
  62. ldap_con.open { }
  63. if self.account.present? && !self.account.include?("$login") && self.account_password.present?
  64. ldap_auth = authenticate_dn(self.account, self.account_password)
  65. raise AuthSourceException.new(l(:error_ldap_bind_credentials)) if !ldap_auth
  66. end
  67. end
  68. rescue *NETWORK_EXCEPTIONS => e
  69. raise AuthSourceException.new(e.message)
  70. end
  71. def auth_method_name
  72. "LDAP"
  73. end
  74. # Returns true if this source can be searched for users
  75. def searchable?
  76. !account.to_s.include?("$login") && %w(login firstname lastname mail).all? {|a| send("attr_#{a}?")}
  77. end
  78. # Searches the source for users and returns an array of results
  79. def search(q)
  80. q = q.to_s.strip
  81. return [] unless searchable? && q.present?
  82. results = []
  83. search_filter = base_filter & Net::LDAP::Filter.begins(self.attr_login, q)
  84. ldap_con = initialize_ldap_con(self.account, self.account_password)
  85. ldap_con.search(:base => self.base_dn,
  86. :filter => search_filter,
  87. :attributes => ['dn', self.attr_login, self.attr_firstname, self.attr_lastname, self.attr_mail],
  88. :size => 10) do |entry|
  89. attrs = get_user_attributes_from_ldap_entry(entry)
  90. attrs[:login] = AuthSourceLdap.get_attr(entry, self.attr_login)
  91. results << attrs
  92. end
  93. results
  94. rescue *NETWORK_EXCEPTIONS => e
  95. raise AuthSourceException.new(e.message)
  96. end
  97. def ldap_mode
  98. case
  99. when tls && verify_peer
  100. :ldaps_verify_peer
  101. when tls && !verify_peer
  102. :ldaps_verify_none
  103. else
  104. :ldap
  105. end
  106. end
  107. def ldap_mode=(ldap_mode)
  108. case ldap_mode.try(:to_sym)
  109. when :ldaps_verify_peer
  110. self.tls = true
  111. self.verify_peer = true
  112. when :ldaps_verify_none
  113. self.tls = true
  114. self.verify_peer = false
  115. else
  116. self.tls = false
  117. self.verify_peer = false
  118. end
  119. end
  120. private
  121. def with_timeout(&block)
  122. timeout = self.timeout
  123. timeout = 20 unless timeout && timeout > 0
  124. Timeout.timeout(timeout) do
  125. return yield
  126. end
  127. rescue Timeout::Error => e
  128. raise AuthSourceTimeoutException.new(e.message)
  129. end
  130. def ldap_filter
  131. if filter.present?
  132. Net::LDAP::Filter.construct(filter)
  133. end
  134. rescue Net::LDAP::Error, Net::LDAP::FilterSyntaxInvalidError
  135. nil
  136. end
  137. def base_filter
  138. filter = Net::LDAP::Filter.eq("objectClass", "*")
  139. if f = ldap_filter
  140. filter = filter & f
  141. end
  142. filter
  143. end
  144. def validate_filter
  145. if filter.present? && ldap_filter.nil?
  146. errors.add(:filter, :invalid)
  147. end
  148. end
  149. def strip_ldap_attributes
  150. [:attr_login, :attr_firstname, :attr_lastname, :attr_mail].each do |attr|
  151. write_attribute(attr, read_attribute(attr).strip) unless read_attribute(attr).nil?
  152. end
  153. end
  154. def initialize_ldap_con(ldap_user, ldap_password)
  155. options = { :host => self.host,
  156. :port => self.port
  157. }
  158. if tls
  159. options[:encryption] = {
  160. :method => :simple_tls,
  161. # Always provide non-empty tls_options, to make sure, that all
  162. # OpenSSL::SSL::SSLContext::DEFAULT_PARAMS as well as the default cert
  163. # store are used.
  164. :tls_options => { :verify_mode => verify_peer? ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE }
  165. }
  166. end
  167. options.merge!(:auth => { :method => :simple, :username => ldap_user, :password => ldap_password }) unless ldap_user.blank? && ldap_password.blank?
  168. Net::LDAP.new options
  169. end
  170. def get_user_attributes_from_ldap_entry(entry)
  171. {
  172. :dn => entry.dn,
  173. :firstname => AuthSourceLdap.get_attr(entry, self.attr_firstname),
  174. :lastname => AuthSourceLdap.get_attr(entry, self.attr_lastname),
  175. :mail => AuthSourceLdap.get_attr(entry, self.attr_mail),
  176. :auth_source_id => self.id
  177. }
  178. end
  179. # Return the attributes needed for the LDAP search. It will only
  180. # include the user attributes if on-the-fly registration is enabled
  181. def search_attributes
  182. if onthefly_register?
  183. ['dn', self.attr_firstname, self.attr_lastname, self.attr_mail]
  184. else
  185. ['dn']
  186. end
  187. end
  188. # Check if a DN (user record) authenticates with the password
  189. def authenticate_dn(dn, password)
  190. if dn.present? && password.present?
  191. initialize_ldap_con(dn, password).bind
  192. end
  193. end
  194. # Get the user's dn and any attributes for them, given their login
  195. def get_user_dn(login, password)
  196. ldap_con = nil
  197. if self.account && self.account.include?("$login")
  198. ldap_con = initialize_ldap_con(self.account.sub("$login", Net::LDAP::DN.escape(login)), password)
  199. else
  200. ldap_con = initialize_ldap_con(self.account, self.account_password)
  201. end
  202. attrs = {}
  203. search_filter = base_filter & Net::LDAP::Filter.eq(self.attr_login, login)
  204. ldap_con.search( :base => self.base_dn,
  205. :filter => search_filter,
  206. :attributes=> search_attributes) do |entry|
  207. if onthefly_register?
  208. attrs = get_user_attributes_from_ldap_entry(entry)
  209. else
  210. attrs = {:dn => entry.dn}
  211. end
  212. logger.debug "DN found for #{login}: #{attrs[:dn]}" if logger && logger.debug?
  213. end
  214. attrs
  215. end
  216. def self.get_attr(entry, attr_name)
  217. if !attr_name.blank?
  218. value = entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name]
  219. value.to_s.force_encoding('UTF-8')
  220. end
  221. end
  222. end