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

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