[api] introduce LDAP StartTLS support
[opensuse:build-service.git] / src / api / lib / active_rbac_mixins / user_mixins.rb
1 module ActiveRbacMixins
2   # The UserMixin module provides the functionality for the User
3   # ActiveRecord class. You can use it the following way: Create a file 
4   # "model/user.rb" in your "RAILS_ENV/app" directory.
5   #
6   # Here, create the User class and import the User mixin modules, 
7   # e.g.:
8   #
9   #   class User < ActiveRecord::Base
10   #     include ActiveRbacMixins::UserMixins::Core
11   #     include ActiveRbacMixins::UserMixins::Validation
12   #
13   #     # insert your custom code here
14   #   end
15   #
16   # This will create a ActiveRecord class you can then extend to your liking (i.e.
17   # just imagine you had written all the stuff that ActiveRbac's User
18   # class provides and you can now write some custom lines below it).
19 module UserMixins
20     module Core
21       @@ldap_search_con = nil
22       # This method is called when the module is included.
23       #
24       # On inclusion, we do a nifty bit of meta programming and make the
25       # including class behave like ActiveRBAC's User class without some
26       # of the validation. Extensive validation can be done by including the 
27       # Validation class.
28       def self.included(base)
29         base.class_eval do
30           # users have a n:m relation to group
31           has_and_belongs_to_many :groups, :uniq => true
32           # users have a n:m relation to roles
33           has_and_belongs_to_many :roles, :uniq => true
34           # users have 0..1 user_registration records assigned to them
35           has_one :user_registration
36
37           # We don't want to assign things to roles and groups in bulk assigns.
38           attr_protected :roles, :groups, :created_at, :updated_at, :last_logged_in_at, :login_failure_count, :password_hash_type
39
40           # This method returns a hash with the the available user states. 
41           # By default it returns the private class constant DEFAULT_STATES.
42           def self.states
43             default_states
44           end
45
46           # This method returns an array with the names of all available
47           # password hash types supported by this User class.
48           def self.password_hash_types
49             default_password_hash_types
50           end
51
52           # When a record object is initialized, we set the state, password
53           # hash type, indicator whether the password has freshly been set 
54           # (@new_password) and the login failure count to 
55           # unconfirmed/false/0 when it has not been set yet.
56           def initialize (attributes = nil)
57             super(attributes)
58
59             self.state = User.states['unconfirmed'] if self.state.nil? 
60             self.password_hash_type = 'md5' if self.password_hash_type.to_s == ''
61
62             @new_password = false if @new_password.nil?
63             @new_hash_type = false if @new_hash_type.nil?
64
65             self.login_failure_count = 0 if self.login_failure_count.nil?
66           end
67
68           # Set the last login time etc. when the record is created at first.
69           def before_create
70             self.last_logged_in_at = Time.now
71           end
72
73           # Override the accessor for the "password_hash_type" property so it sets
74           # the "@new_hash_type" private property to signal that the password's
75           # hash method has been changed. Changing the password hash type is only
76           # possible if a new password has been provided.
77           def password_hash_type=(value)
78             write_attribute(:password_hash_type, value)
79             @new_hash_type = true
80           end
81
82           # After saving, we want to set the "@new_hash_type" value set to false
83           # again.
84           after_save '@new_hash_type = false'
85
86           # Add accessors for "new_password" property. This boolean property is set 
87           # to true when the password has been set and validation on this password is
88           # required.
89           attr_accessor :new_password
90
91           # Generate accessors for the password confirmation property.
92           attr_accessor :password_confirmation
93
94           # Overriding the default accessor to update @new_password on setting this
95           # property.
96           def password=(value)
97             write_attribute(:password, value)
98             @new_password = true
99           end
100           
101           # Returns true if the password has been set after the User has been loaded
102           # from the database and false otherwise
103           def new_password?
104             @new_password == true
105           end
106
107           # Method to update the password and confirmation at the same time. Call
108           # this method when you update the password from code and don't need 
109           # password_confirmation - which should really only be used when data
110           # comes from forms. 
111           #
112           # A ussage example:
113           #
114           #   user = User.find(1)
115           #   user.update_password "n1C3s3cUreP4sSw0rD"
116           #   user.save
117           #
118           def update_password(pass)
119             self.password_confirmation = pass
120             self.password = pass
121           end
122
123           # After saving the object into the database, the password is not new any more.
124           after_save '@new_password = false'
125
126           # This method writes the attribute "password" to the hashed version. It is 
127           # called in the after_validation hook set by the "after_validation" command
128           # above.
129           # The password is only encrypted when no errors occurred on validation, the
130           # password is new and the password is not nil.
131           # This method also sets the "password_salt" property's value used in 
132           # User#hash_string.
133           # After encryption, the password's "new" state is reset and the confirmation
134           # is cleared. The password hash's type will also be set to "not new" since
135           # we get problems with double validation (as it happens when using save!)
136           # otherwise.
137           def encrypt_password
138             if errors.count == 0 and @new_password and not password.nil?
139               # generate a new 10-char long hash only Base64 encoded so things are compatible
140               self.password_salt = [Array.new(10){rand(256).chr}.join].pack("m")[0..9]; 
141
142               # write encrypted password to object property
143               write_attribute(:password, hash_string(password))
144
145               # mark password as "not new" any more
146               @new_password = false
147               password_confirmation = nil
148               
149               # mark the hash type as "not new" any more
150               @new_hash_type = false
151             end
152           end
153
154           # This method returns all roles assigned to the given user - including
155           # the ones he gets by being assigned a child role (i.e. the parents)
156           # and the one he gets through his groups (inheritance is also considered)
157           # here.
158           def all_roles
159             result = Array.new
160
161             for role in self.roles
162               result << role.ancestors_and_self
163             end
164
165             for group in self.groups
166               result << group.all_roles
167             end
168
169             result.flatten!
170             result.uniq!
171
172             return result
173           end
174
175           # This method returns all groups assigned to the given user - including
176           # the ones he gets by being assigned through group inheritance.
177           def all_groups
178             result = Array.new
179
180             for group in self.groups
181               result << group.ancestors_and_self
182             end
183
184             result.flatten!
185             result.uniq!
186
187             return result
188           end
189    
190           # This method returns all groups assigned to the given user via ldap - including
191           # the ones he gets by being assigned through group inheritance.
192           def all_groups_ldap(group_ldap)
193             result = Array.new
194             for group in group_ldap
195               result << group.ancestors_and_self
196             end
197
198             result.flatten!
199             result.uniq!
200
201             return result
202           end
203
204           # This method returns true if the user is assigned the role with one of the
205           # role titles given as parameters. False otherwise.
206           def has_role?(*role_titles)
207             obj = all_roles.detect do |role| 
208                     role_titles.include?(role.title)
209                   end
210             
211             return !obj.nil?
212           end
213
214           # This method returns a list of all the StaticPermission entities that
215           # have been assigned to this user through his roles.
216           def all_static_permissions
217             permissions = Array.new
218
219             all_roles.each do |role|
220               permissions.concat(role.static_permissions)
221             end
222
223             return permissions
224           end
225
226           # This method returns true if the user is granted the permission with one
227           # of the given permission titles.
228           def has_permission?(*permission_titles)
229             all_roles.detect do |role| 
230               role.static_permissions.detect do |permission|
231                 permission_titles.include?(permission.title)
232               end
233             end
234           end
235           
236           # Returns false. is_anonymous? will only return true on AnonymousUser
237           # objects.
238           def is_anonymous?
239             false
240           end
241           
242
243           # This method creates a new registration token for the current user. Raises 
244           # a MultipleRegistrationTokens Exception if the user already has a 
245           # registration token assigned to him.
246           #
247           # Use this method instead of creating user_registration objects directly!
248           def create_user_registration
249             raise unless user_registration.nil?
250
251             token = UserRegistration.new
252             self.user_registration = token
253           end
254
255           # This method expects the token for the current user. If the token is
256           # correct, the user's state will be set to "confirmed" and the associated
257           # "user_registration" record will be removed.
258           # Returns "true" on success and "false" on failure/or the user is already
259           # confirmed and/or has no "user_registration" record.
260           def confirm_registration(token)
261             return false if self.user_registration.nil?
262             return false if user_registration.token != token
263             return false unless state_transition_allowed?(state, User.states['confirmed'])
264
265             self.state = User.states['confirmed']
266             self.save!
267             user_registration.destroy
268
269             return true
270           end
271
272           # Returns the default state of new User objects.
273           def self.default_state
274             User.states['unconfirmed']
275           end
276
277           # Returns true when users with the given state may log in. False otherwise.
278           # The given parameter must be an integer.
279           def self.state_allows_login?(state)
280             [ User.states['confirmed'], User.states['retrieved_password'] ].include?(state)
281           end
282
283           # Overwrite the state setting so it backs up the initial state from
284           # the database.
285           def state=(value)
286             @old_state = state if @old_state.nil?
287             write_attribute(:state, value)
288           end
289
290           # Overriding this method to make "login" visible as "User name". This is called in
291           # forms to create error messages.
292           def self.human_attribute_name (attr)
293             return case attr
294                    when 'login' then 'User name'
295                    else attr.humanize
296                    end
297           end
298
299           # This static method removes all users with state "unconfirmed" and expired
300           # registration tokens.
301           def self.purge_users_with_expired_registration
302             registrations = UserRegistration.find :all,
303                                                   :conditions => [ 'expires_at < ?', Time.now.ago(2.days) ]
304             registrations.each do |registration|
305               registration.user.destroy
306             end
307           end
308
309           # This static method tries to find a user with the given login and password
310           # in the database. Returns the user or nil if he could not be found
311           def self.find_with_credentials(login, password)
312             # Find user
313             user = User.find :first,
314                              :conditions => [ 'login = ?', login ]
315
316             # If the user could be found and the passwords equal then return the user
317             if not user.nil? and user.password_equals? password
318               if user.login_failure_count > 0
319                 user.login_failure_count = 0
320                 self.execute_without_timestamps { user.save! }
321               end
322
323               return user
324             end
325
326             # Otherwise increase the login count - if the user could be found - and return nil
327             if not user.nil?
328               user.login_failure_count = user.login_failure_count + 1
329               self.execute_without_timestamps { user.save! }
330             end
331
332             return nil
333           end
334
335           # This static method tries to update the entry with the given info in the 
336           # active directory server.  Return the error msg if any error occurred
337           def self.update_entry_ldap(login, newlogin, newemail, newpassword)
338             logger.debug( " Modifying #{login} to #{newlogin} #{newemail} using ldap" )
339             
340             if @@ldap_search_con.nil?
341               @@ldap_search_con = initialize_ldap_con(LDAP_SEARCH_USER, LDAP_SEARCH_AUTH)
342             end
343             ldap_con = @@ldap_search_con
344             if ldap_con.nil?
345               logger.debug( "Unable to connect to LDAP server" )
346               return "Unable to connect to LDAP server"
347             end
348             user_filter = "(#{LDAP_SEARCH_ATTR}=#{login})"
349             dn = String.new
350             ldap_con.search( LDAP_SEARCH_BASE, LDAP::LDAP_SCOPE_SUBTREE, user_filter ) do |entry|
351               dn = entry.dn
352             end
353             if dn.empty?
354               logger.debug( "User not found in ldap" )
355               return "User not found in ldap"
356             end
357
358             # Update mail/password info
359             entry = [
360                   LDAP.mod(LDAP::LDAP_MOD_REPLACE,LDAP_MAIL_ATTR,[newemail]),
361                   ]
362             if newpassword
363               case LDAP_AUTH_MECH
364               when :cleartext then
365                 entry << LDAP.mod(LDAP::LDAP_MOD_REPLACE,LDAP_AUTH_ATTR,[newpassword])
366               when :md5 then
367                 require 'digest/md5'
368                 require 'base64'
369                 entry << LDAP.mod(LDAP::LDAP_MOD_REPLACE,LDAP_AUTH_ATTR,["{MD5}"+Base64.b64encode(Digest::MD5.digest(newpassword)).chomp])
370               end
371             end
372             begin
373               ldap_con.modify(dn, entry)
374             rescue LDAP::ResultError
375               logger.debug("Error #{ldap_con.err} for #{login} mail/password changing")
376               return "Failed to update entry for #{login}: error #{ldap_con.err}"
377             end
378
379             # Update the dn name if it is changed
380             if not login == newlogin
381               begin
382                 ldap_con.modrdn(dn,"#{LDAP_NAME_ATTR}=#{newlogin}", true)
383               rescue LDAP::ResultError
384                 logger.debug("Error #{ldap_con.err} for #{login} dn name changing")
385                 return "Failed to update dn name for #{login}: error #{ldap_con.err}"
386               end
387             end
388
389             return
390           end
391
392           # This static method tries to add the new entry with the given name/password/mail info in the 
393           # active directory server.  Return the error msg if any error occurred
394           def self.new_entry_ldap(login, password, mail)
395             require 'ldap'
396             logger.debug( "Add new entry for #{login} using ldap" )
397             if @@ldap_search_con.nil?
398               @@ldap_search_con = initialize_ldap_con(LDAP_SEARCH_USER, LDAP_SEARCH_AUTH)
399             end
400             ldap_con = @@ldap_search_con
401             if ldap_con.nil?
402               logger.debug( "Unable to connect to LDAP server" )
403               return "Unable to connect to LDAP server"
404             end
405             case LDAP_AUTH_MECH
406             when :cleartext then
407               ldap_password = password
408             when :md5 then
409               require 'digest/md5'
410               require 'base64'
411               ldap_password = "{MD5}"+Base64.b64encode(Digest::MD5.digest(password)).chomp
412             end
413             entry = [
414               LDAP.mod(LDAP::LDAP_MOD_ADD,'objectclass',LDAP_OBJECT_CLASS),
415               LDAP.mod(LDAP::LDAP_MOD_ADD,LDAP_NAME_ATTR,[login]),
416               LDAP.mod(LDAP::LDAP_MOD_ADD,LDAP_AUTH_ATTR,[ldap_password]),
417               LDAP.mod(LDAP::LDAP_MOD_ADD,LDAP_MAIL_ATTR,[mail]),       
418             ]
419             # Added required sn attr
420             if defined?( LDAP_SN_ATTR_REQUIRED ) && LDAP_SN_ATTR_REQUIRED == :on
421               entry << LDAP.mod(LDAP::LDAP_MOD_ADD,'sn',[login])
422             end
423
424             begin
425               ldap_con.add("#{LDAP_NAME_ATTR}=#{login},#{LDAP_ENTRY_BASE}", entry)
426             rescue LDAP::ResultError
427               logger.debug("Error #{ldap_con.err} for #{login}")
428               return "Failed to add a new entry for #{login}: error #{ldap_con.err}"
429             end
430             return
431           end
432
433           # This static method tries to delete the entry with the given login in the 
434           # active directory server.  Return the error msg if any error occurred
435           def self.delete_entry_ldap(login)
436             logger.debug( "Deleting #{login} using ldap" )
437             if @@ldap_search_con.nil?
438               @@ldap_search_con = initialize_ldap_con(LDAP_SEARCH_USER, LDAP_SEARCH_AUTH)
439             end
440             ldap_con = @@ldap_search_con
441             if ldap_con.nil?
442               logger.debug( "Unable to connect to LDAP server" )
443               return "Unable to connect to LDAP server"
444             end
445             user_filter = "(#{LDAP_SEARCH_ATTR}=#{login})"
446             dn = String.new
447             ldap_con.search( LDAP_SEARCH_BASE, LDAP::LDAP_SCOPE_SUBTREE, user_filter ) do |entry|
448               dn = entry.dn
449             end
450             if dn.empty?
451               logger.debug( "User not found in ldap" )
452               return "User not found in ldap"
453             end
454             begin
455               ldap_con.delete(dn)
456             rescue LDAP::ResultError
457               logger.debug( "Failed to delete: error #{ldap_con.err} for #{login}" )
458               return "Failed to delete the entry #{login}: error #{ldap_con.err}"
459             end
460             return
461           end
462
463           # Check if ldap group support is enabled?
464           def self.ldapgroup_enabled?
465             if defined?( LDAP_MODE ) && LDAP_MODE == :on
466               if defined?( LDAP_GROUP_SUPPORT ) && LDAP_GROUP_SUPPORT == :on
467                 return true
468               end
469             end
470             return false
471           end
472
473           # This static method tries to find a group with the given gorup_title to check whether the group is in the LDAP server.
474           def self.find_group_with_ldap(group)
475             if defined?( LDAP_GROUP_OBJECTCLASS_ATTR )
476               filter = "(&(#{LDAP_GROUP_TITLE_ATTR}=#{group})(objectclass=#{LDAP_GROUP_OBJECTCLASS_ATTR}))"
477             else
478               filter = "(#{LDAP_GROUP_TITLE_ATTR}=#{group})"
479             end
480             result = search_ldap(LDAP_GROUP_SEARCH_BASE, filter)
481             if result.nil? 
482               logger.debug( "Fail to find group: #{group} in LDAP" )
483               return false
484             else
485               logger.debug( "group dn: #{result[0]}" )
486               return true
487             end
488           end
489
490           # This static method performs the search with the given search_base, filter
491           def self.search_ldap(search_base, filter, required_attr = nil)
492             if @@ldap_search_con.nil?
493               @@ldap_search_con = initialize_ldap_con(LDAP_SEARCH_USER, LDAP_SEARCH_AUTH)
494             end
495             ldap_con = @@ldap_search_con
496             if ldap_con.nil?
497               logger.debug( "Unable to connect to LDAP server" )
498               return nil
499             end
500             logger.debug( "Search: #{filter}" )
501             result = Array.new
502             ldap_con.search( search_base, LDAP::LDAP_SCOPE_SUBTREE, filter ) do |entry|
503               result << entry.dn
504               result << entry.attrs
505               if required_attr and entry.attrs.include?(required_attr)
506                 result << entry.vals(required_attr)
507               end
508             end
509             if result.empty?
510               return nil
511             else
512               return result
513             end
514           end
515            
516           # This static method performs the search with the given grouplist, user to return the groups that the user in 
517           def self.render_grouplist_ldap(grouplist, user = nil)
518             result = Array.new
519             if @@ldap_search_con.nil?
520               @@ldap_search_con = initialize_ldap_con(LDAP_SEARCH_USER, LDAP_SEARCH_AUTH)
521             end
522             ldap_con = @@ldap_search_con
523             if ldap_con.nil?
524               logger.debug( "Unable to connect to LDAP server" )
525               return result
526             end
527
528             if not user.nil?
529               # search user
530               if defined?( LDAP_USER_FILTER )
531                 filter = "(&(#{LDAP_SEARCH_ATTR}=#{user})#{LDAP_USER_FILTER})"
532               else
533                 filter = "(#{LDAP_SEARCH_ATTR}=#{user})"
534               end
535               user_dn = String.new
536               user_memberof_attr = String.new
537               ldap_con.search( LDAP_SEARCH_BASE, LDAP::LDAP_SCOPE_SUBTREE, filter ) do |entry|
538                 user_dn = entry.dn
539                 if defined?( LDAP_USER_MEMBEROF_ATTR ) && entry.attrs.include?( LDAP_USER_MEMBEROF_ATTR )
540                   user_memberof_attr=entry.vals(LDAP_USER_MEMBEROF_ATTR)
541                 end            
542               end
543               if user_dn.empty?
544                 logger.debug( "Failed to find #{user} in ldap" )
545                 return result
546               end
547               logger.debug( "User dn: #{user_dn} user_memberof_attr: #{user_memberof_attr}" )
548             end
549
550             group_dn = String.new
551             group_member_attr = String.new
552             grouplist.each do |eachgroup|
553               if eachgroup.kind_of? String
554                 group = eachgroup
555               end
556               if eachgroup.kind_of? Group
557                 group = eachgroup.title
558               end
559
560               unless group.kind_of? String
561                 raise ArgumentError, "illegal parameter type to user#render_grouplist_ldap?: #{eachgroup.class.name}"
562               end
563
564               # search group
565               if defined?( LDAP_GROUP_OBJECTCLASS_ATTR )
566                 filter = "(&(#{LDAP_GROUP_TITLE_ATTR}=#{group})(objectclass=#{LDAP_GROUP_OBJECTCLASS_ATTR}))" 
567               else
568                 filter = "(#{LDAP_GROUP_TITLE_ATTR}=#{group})"
569               end
570               
571               # clean group_dn, group_member_attr
572               group_dn = ""
573               group_member_attr = ""
574               logger.debug( "Search group: #{filter}" )         
575               ldap_con.search( LDAP_GROUP_SEARCH_BASE, LDAP::LDAP_SCOPE_SUBTREE, filter ) do |entry|
576                 group_dn = entry.dn
577                 if defined?( LDAP_GROUP_MEMBER_ATTR ) && entry.attrs.include?(LDAP_GROUP_MEMBER_ATTR)
578                   group_member_attr = entry.vals(LDAP_GROUP_MEMBER_ATTR)
579                 end
580               end
581               if group_dn.empty?
582                 logger.debug( "Failed to find #{group} in ldap" )
583                 next
584               end
585               
586               if user.nil?
587                 result << eachgroup
588                 next
589               end
590
591               # user memberof attr exist?
592               if user_memberof_attr and user_memberof_attr.include?(group_dn)
593                 result << eachgroup
594                 logger.debug( "#{user} is in #{group}" )
595                 next
596               end
597               # group member attr exist?
598               if group_member_attr and group_member_attr.include?(user_dn)
599                 result << eachgroup
600                 logger.debug( "#{user} is in #{group}" )
601                 next
602               end
603               logger.debug("#{user} is not in #{group}")
604             end
605
606             return result
607           end
608
609           # This static method tries to update the password with the given login in the 
610           # active directory server.  Return the error msg if any error occurred
611           def self.change_password_ldap(login, password)
612             if @@ldap_search_con.nil?
613               @@ldap_search_con = initialize_ldap_con(LDAP_SEARCH_USER, LDAP_SEARCH_AUTH)
614             end
615             ldap_con = @@ldap_search_con
616             if ldap_con.nil?
617               logger.debug( "Unable to connect to LDAP server" )
618               return "Unable to connect to LDAP server"
619             end
620             user_filter = "(#{LDAP_SEARCH_ATTR}=#{login})"
621             dn = String.new
622             ldap_con.search( LDAP_SEARCH_BASE, LDAP::LDAP_SCOPE_SUBTREE, user_filter ) do |entry|
623               dn = entry.dn
624             end
625             if dn.empty?
626               logger.debug( "User not found in ldap" )
627               return "User not found in ldap"
628             end
629
630             case LDAP_AUTH_MECH
631             when :cleartext then
632               ldap_password = password
633             when :md5 then
634               require 'digest/md5'
635               require 'base64'
636               ldap_password = "{MD5}"+Base64.b64encode(Digest::MD5.digest(password)).chomp
637             end
638             entry = [
639               LDAP.mod(LDAP::LDAP_MOD_REPLACE, LDAP_AUTH_ATTR, [ldap_password]),
640             ]
641             begin
642               ldap_con.modify(dn, entry)
643             rescue LDAP::ResultError
644               logger.debug("Error #{ldap_con.err} for #{login}")
645               return "#{ldap_con.err}"
646             end
647
648             return
649           end
650
651
652           # This static method tries to find a user with the given login and
653           # password in the active directory server.  Returns nil unless 
654           # credentials are correctly found using LDAP.
655           def self.find_with_ldap(login, password)
656             logger.debug( "Looking for #{login} using ldap" )
657             ldap_info = Array.new
658             # use cache to check the password firstly
659             key="ldap_cache_userpasswd:" + login
660             require 'digest/md5'
661             if Rails.cache.exist?(key)
662               ar = Rails.cache.read(key)
663               if ar[0] == Digest::MD5.digest(password)
664                 ldap_info[0] = ar[1]
665                 ldap_info[1] = ar[2]
666                 logger.debug("login success for checking with ldap cache")
667                 return ldap_info
668               end 
669             end
670
671             if @@ldap_search_con.nil?
672               @@ldap_search_con = initialize_ldap_con(LDAP_SEARCH_USER, LDAP_SEARCH_AUTH)
673             end
674             ldap_con = @@ldap_search_con
675             if ldap_con.nil?
676               logger.debug( "Unable to connect to LDAP server" )
677               return nil
678             end
679
680             if defined?( LDAP_USER_FILTER )
681               user_filter = "(&(#{LDAP_SEARCH_ATTR}=#{login})#{LDAP_USER_FILTER})"
682             else
683               user_filter = "(#{LDAP_SEARCH_ATTR}=#{login})"
684             end
685             logger.debug( "Search for #{user_filter}" )
686             dn = String.new
687             ldap_password = String.new
688             begin
689               ldap_con.search( LDAP_SEARCH_BASE, LDAP::LDAP_SCOPE_SUBTREE, user_filter ) do |entry|
690                 dn = entry.dn
691                 ldap_info[0] = String.new(entry[LDAP_MAIL_ATTR][0])
692                 if defined?( LDAP_AUTHENTICATE ) && LDAP_AUTHENTICATE == :local
693                   if entry[LDAP_AUTH_ATTR] then
694                     ldap_password = entry[LDAP_AUTH_ATTR][0]
695                     logger.debug( "Get auth_attr:#{ldap_password}" )
696                   else
697                     logger.debug( "Failed to get attr:#{LDAP_AUTH_ATTR}" )
698                   end
699                 end
700               end
701             rescue
702               logger.debug( "Search failed:  error #{ @@ldap_search_con.err}" )
703               @@ldap_search_con.unbind
704               @@ldap_search_con = nil
705               return nil
706             end
707             if dn.empty?
708               logger.debug( "User not found in ldap" )
709               return nil
710             end
711             # Attempt to authenticate user
712             case LDAP_AUTHENTICATE
713             when :local then
714               authenticated = false
715               case LDAP_AUTH_MECH
716               when :cleartext then
717                 if ldap_password == password then
718                   authenticated = true
719                 end
720               when :md5 then
721                 require 'digest/md5'
722                 require 'base64'
723                 if ldap_password == "{MD5}"+Base64.b64encode(Digest::MD5.digest(password)) then
724                   authenticated = true
725                 end
726               end
727               if authenticated == true
728                 ldap_info[0] = String.new(entry[LDAP_MAIL_ATTR][0])
729                 ldap_info[1] = String.new(entry[LDAP_NAME_ATTR][0])
730               end
731             when :ldap then
732               # Don't match the passwd locally, try to bind to the ldap server
733               user_con= initialize_ldap_con(dn,password)
734               if user_con.nil?
735                 logger.debug( "Unable to connect to LDAP server as #{dn} using credentials supplied" )
736               else
737                 # Redo the search as the user for situations where the anon search may not be able to see attributes
738                 user_con.search( LDAP_SEARCH_BASE, LDAP::LDAP_SCOPE_SUBTREE,  user_filter ) do |entry|
739                   if entry[LDAP_MAIL_ATTR] then 
740                     ldap_info[0] = String.new(entry[LDAP_MAIL_ATTR][0])
741                   end
742                   if entry[LDAP_NAME_ATTR] then
743                     ldap_info[1] = String.new(entry[LDAP_NAME_ATTR][0])
744                   else
745                     ldap_info[1] = login
746                   end
747                 end
748                 user_con.unbind()
749               end
750             end
751             Rails.cache.write(key, [Digest::MD5.digest(password), ldap_info[0], ldap_info[1]], :expires_in => 2.minute)
752             logger.debug( "login success for checking with ldap server" )
753             ldap_info
754           end
755
756           # This method checks whether the given value equals the password when
757           # hashed with this user's password hash type. Returns a boolean.
758           def password_equals?(value)
759             return hash_string(value) == self.password
760           end
761
762           # Sets the last login time and saves the object. Note: Must currently be 
763           # called explicitely!
764           def did_log_in
765             self.last_logged_in_at = DateTime.now
766             self.class.execute_without_timestamps { save }
767           end
768
769           # Returns true if the the state transition from "from" state to "to" state 
770           # is valid. Returns false otherwise. +new_state+ must be the integer value 
771           # of the state as returned by +User.states['state_name']+.
772           #
773           # Note that currently no permission checking is included here; It does not 
774           # matter what permissions the currently logged in user has, only that the
775           # state transition is legal in principle.
776           def state_transition_allowed?(from, to)
777             from = from.to_i
778             to = to.to_i
779
780             return true if from == to # allow keeping state
781
782             return case from
783               when User.states['unconfirmed']
784                 true
785               when User.states['confirmed']
786                 [ User.states['retrieved_password'], User.states['locked'], User.states['deleted'] ].include?(to)
787               when User.states['locked']
788                 [ User.states['confirmed'], User.states['deleted'] ].include?(to)
789               when User.states['deleted']
790                 [ User.states['confirmed'] ].include?(to)
791               when User.states['retrieved_password']
792                 [ User.states['confirmed'], User.states['locked'], User.states['deleted'] ].include?(to)
793               when 0
794                 User.states.value?(to)
795               else
796                 false
797             end
798           end
799
800           # After validation, the password should be encrypted  
801           after_validation :encrypt_password
802
803           protected
804             # This method allows to execute a block while deactivating timestamp
805             # updating.
806             def self.execute_without_timestamps
807               old_state = ActiveRecord::Base.record_timestamps
808               ActiveRecord::Base.record_timestamps = false
809
810               yield
811
812               ActiveRecord::Base.record_timestamps = old_state
813             end
814
815             # Model Validation
816
817             validates_presence_of   :login, :email, :password, :password_hash_type, :state,
818                                     :message => 'must be given'
819
820             validates_uniqueness_of :login, 
821                                     :message => 'is the name of an already existing user.'
822
823             # Overriding this method to do some more validation: Password equals 
824             # password_confirmation, state an password hash type being in the range
825             # of allowed values.
826             def validate
827               # validate state and password has type to be in the valid range of values
828               errors.add(:password_hash_type, "must be in the list of hash types.") unless User.password_hash_types.include? password_hash_type
829               # check that the state transition is valid
830               errors.add(:state, "must be a valid new state from the current state.") unless state_transition_allowed?(@old_state, state)
831
832               # validate the password
833               if @new_password and not password.nil?
834                 errors.add(:password, 'must match the confirmation.') unless password_confirmation == password
835               end
836
837               # check that the password hash type has not been set if no new password
838               # has been provided
839               if @new_hash_type and (!@new_password or password.nil?) then
840                 errors.add(:password_hash_type, 'cannot be changed unless a new password has been provided.')
841               end
842             end
843
844           private
845             # This method returns a hash which contains a mapping of user states 
846             # valid by default and their description.
847             def self.default_states
848               {
849                 'unconfirmed' => 1,
850                 'confirmed' => 2,
851                 'locked' => 3,
852                 'deleted' => 4,
853                 # The user has just retrieved his password and he must now
854                 # it. The user cannot anything in this state but change his
855                 # password after having logged in and retrieve another one.
856                 'retrieved_password' => 5
857               }
858             end
859
860             # This method returns an array which contains all valid hash types.
861             def self.default_password_hash_types
862               [ 'md5' ]
863             end
864
865             # Hashes the given parameter by the selected hashing method. It uses the
866             # "password_salt" property's value to make the hashing more secure.
867             def hash_string(value)
868               return case password_hash_type
869                      when 'md5' then Digest::MD5.hexdigest(value + self.password_salt)
870                      end
871             end 
872
873             # this method returns a ldap object using the provided user name
874             # and password
875             def self.initialize_ldap_con(user_name, password)
876               return nil unless defined?( LDAP_SERVERS )
877               ldap_servers = LDAP_SERVERS.split(":")
878               ping = false
879               server = nil
880               count = 0
881               
882               max_ldap_attempts = defined?( LDAP_MAX_ATTEMPTS ) ? LDAP_MAX_ATTEMPTS : 10
883               
884               while !ping and count < max_ldap_attempts
885                 count += 1
886                 server = ldap_servers[rand(ldap_servers.length)]
887                 # Ruby only contains TCP echo ping.  Use system ping for real ICMP ping.
888                 ping = system("ping -c 1 #{server} >/dev/null 2>/dev/null")
889               end
890               
891               if count == max_ldap_attempts
892                 logger.debug("Unable to ping to any LDAP server: #{LDAP_SERVERS}")
893                 return nil
894               end
895
896               logger.debug( "Connecting to #{server} as '#{user_name}'" )
897               begin
898                 if defined?( LDAP_SSL ) && LDAP_SSL == :on
899                   port = defined?( LDAP_PORT ) ? LDAP_PORT : 636
900                   conn = LDAP::SSLConn.new( server, port)
901                 else
902                   port = defined?( LDAP_PORT ) ? LDAP_PORT : 389
903                   # Use LDAP StartTLS. By default start_tls is off.
904                   if defined?( LDAP_START_TLS ) ? LDAP_START_TLS == :on
905                     conn = LDAP::SSLConn.new( server, port, true)
906                   else
907                     conn = LDAP::Conn.new( server, port)
908                   end
909                 end
910                 conn.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
911                 if defined?( LDAP_REFERRALS ) && LDAP_REFERRALS == :off
912                   conn.set_option(LDAP::LDAP_OPT_REFERRALS, LDAP::LDAP_OPT_OFF)
913                 end
914                 conn.bind(user_name, password)
915               rescue LDAP::ResultError
916                 if not conn.nil?
917                   conn.unbind()
918                 end
919                 logger.debug( "Not bound:  error #{conn.err} for #{user_name}" )
920                 return nil
921               end
922               logger.debug( "Bound as #{user_name}" )
923               return conn
924             end
925         end
926       end
927     end
928
929     module Validation
930       # This method is called when the module is included.
931       #
932       # On inclusion, we do a nifty bit of meta programming and make the
933       # including class validate as ActiveRBAC's User class does.
934       def self.included(base)
935         base.class_eval do
936           validates_format_of     :login, 
937                                   :with => %r{^[\w \$\^\-\.#\*\+&'"]*$}, 
938                                   :message => 'must not contain invalid characters.'
939           validates_length_of     :login, 
940                                   :in => 2..100, :allow_nil => true,
941                                   :too_long => 'must have less than 100 characters.', 
942                                   :too_short => 'must have more than two characters.'
943
944           # We want a valid email address. Note that the checking done here is very
945           # rough. Email adresses are hard to validate now domain names may include
946           # language specific characters and user names can be about anything anyway.
947           # However, this is not *so* bad since users have to answer on their email
948           # to confirm their registration.
949           validates_format_of :email, 
950                               :with => %r{^([\w\-\.\#\$%&!?*\'\+=(){}|~_]+)@([0-9a-zA-Z\-\.\#\$%&!?*\'=(){}|~]+)+$},
951                               :message => 'must be a valid email address.'
952
953           # We want to validate the format of the password and only allow alphanumeric
954           # and some punctiation characters.
955           # The format must only be checked if the password has been set and the record
956           # has not been stored yet and it has actually been set at all. Make sure you
957           # include this condition in your :if parameter to validates_format_of when
958           # overriding the password format validation.
959           validates_format_of :password,
960                               :with => %r{^[\w\.\- !?(){}|~*_]+$},
961                               :message => 'must not contain invalid characters.',
962                               :if => Proc.new { |user| user.new_password? and not user.password.nil? }
963
964           # We want the password to have between 6 and 64 characters.
965           # The length must only be checked if the password has been set and the record
966           # has not been stored yet and it has actually been set at all. Make sure you
967           # include this condition in your :if parameter to validates_length_of when
968           # overriding the length format validation.
969           validates_length_of :password,
970                               :within => 6..64,
971                               :too_long => 'must have between 6 and 64 characters.',
972                               :too_short => 'must have between 6 and 64 characters.',
973                               :if => Proc.new { |user| user.new_password? and not user.password.nil? }
974         end
975       end
976     end
977 end
978 end