comparison app/models/user.rb @ 514:7eba09d624db live

Merge
author Chris Cannam
date Thu, 14 Jul 2011 10:50:53 +0100
parents 753f1380d6bc
children ec7c78040115
comparison
equal deleted inserted replaced
512:b9aebdd7dd40 514:7eba09d624db
1 # Redmine - project management software 1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 # 3 #
4 # This program is free software; you can redistribute it and/or 4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License 5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2 6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version. 7 # of the License, or (at your option) any later version.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 require "digest/sha1" 18 require "digest/sha1"
19 19
20 class User < Principal 20 class User < Principal
21 21 include Redmine::SafeAttributes
22
22 # Account statuses 23 # Account statuses
23 STATUS_ANONYMOUS = 0 24 STATUS_ANONYMOUS = 0
24 STATUS_ACTIVE = 1 25 STATUS_ACTIVE = 1
25 STATUS_REGISTERED = 2 26 STATUS_REGISTERED = 2
26 STATUS_LOCKED = 3 27 STATUS_LOCKED = 3
32 :lastname_coma_firstname => '#{lastname}, #{firstname}', 33 :lastname_coma_firstname => '#{lastname}, #{firstname}',
33 :username => '#{login}' 34 :username => '#{login}'
34 } 35 }
35 36
36 MAIL_NOTIFICATION_OPTIONS = [ 37 MAIL_NOTIFICATION_OPTIONS = [
37 [:all, :label_user_mail_option_all], 38 ['all', :label_user_mail_option_all],
38 [:selected, :label_user_mail_option_selected], 39 ['selected', :label_user_mail_option_selected],
39 [:none, :label_user_mail_option_none], 40 ['only_my_events', :label_user_mail_option_only_my_events],
40 [:only_my_events, :label_user_mail_option_only_my_events], 41 ['only_assigned', :label_user_mail_option_only_assigned],
41 [:only_assigned, :label_user_mail_option_only_assigned], 42 ['only_owner', :label_user_mail_option_only_owner],
42 [:only_owner, :label_user_mail_option_only_owner] 43 ['none', :label_user_mail_option_none]
43 ] 44 ]
44 45
45 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)}, 46 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
46 :after_remove => Proc.new {|user, group| group.user_removed(user)} 47 :after_remove => Proc.new {|user, group| group.user_removed(user)}
47 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify 48 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
48 has_many :changesets, :dependent => :nullify 49 has_many :changesets, :dependent => :nullify
49 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference' 50 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
50 has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'" 51 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
51 has_one :api_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='api'" 52 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
52 belongs_to :auth_source 53 belongs_to :auth_source
53 54
54 has_one :ssamr_user_detail, :dependent => :destroy, :class_name => 'SsamrUserDetail' 55 has_one :ssamr_user_detail, :dependent => :destroy, :class_name => 'SsamrUserDetail'
55 accepts_nested_attributes_for :ssamr_user_detail 56 accepts_nested_attributes_for :ssamr_user_detail
56 57
60 acts_as_customizable 61 acts_as_customizable
61 62
62 attr_accessor :password, :password_confirmation 63 attr_accessor :password, :password_confirmation
63 attr_accessor :last_before_login_on 64 attr_accessor :last_before_login_on
64 # Prevents unauthorized assignments 65 # Prevents unauthorized assignments
65 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password, :group_ids 66 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
66 67
67 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) } 68 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
68 69
69 # TODO: is this validation correct validates_presence_of :ssamr_user_detail 70 # TODO: is this validation correct validates_presence_of :ssamr_user_detail
70 71
71 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }, :case_sensitive => false 72 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }, :case_sensitive => false
72 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false 73 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
73 # Login must contain lettres, numbers, underscores only 74 # Login must contain lettres, numbers, underscores only
74 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i 75 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
75 validates_length_of :login, :maximum => 30 76 validates_length_of :login, :maximum => 30
76 validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-\.]*$/i
77 validates_length_of :firstname, :lastname, :maximum => 30 77 validates_length_of :firstname, :lastname, :maximum => 30
78 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true 78 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
79 validates_length_of :mail, :maximum => 60, :allow_nil => true 79 validates_length_of :mail, :maximum => 60, :allow_nil => true
80 validates_confirmation_of :password, :allow_nil => true 80 validates_confirmation_of :password, :allow_nil => true
81 81 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
82
83 before_destroy :remove_references_before_destroy
84
82 validates_acceptance_of :terms_and_conditions, :on => :create, :message => :must_accept_terms_and_conditions 85 validates_acceptance_of :terms_and_conditions, :on => :create, :message => :must_accept_terms_and_conditions
83 86
87 named_scope :in_group, lambda {|group|
88 group_id = group.is_a?(Group) ? group.id : group.to_i
89 { :conditions => ["#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id] }
90 }
91 named_scope :not_in_group, lambda {|group|
92 group_id = group.is_a?(Group) ? group.id : group.to_i
93 { :conditions => ["#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id] }
94 }
95
84 def before_create 96 def before_create
85 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank? 97 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
86 true 98 true
87 end 99 end
88 100
89 def before_save 101 def before_save
90 # update hashed_password if password was set 102 # update hashed_password if password was set
91 self.hashed_password = User.hash_password(self.password) if self.password && self.auth_source_id.blank? 103 if self.password && self.auth_source_id.blank?
104 salt_password(password)
105 end
92 end 106 end
93 107
94 def reload(*args) 108 def reload(*args)
95 @name = nil 109 @name = nil
110 @projects_by_role = nil
96 super 111 super
97 end 112 end
98 113
99 def mail=(arg) 114 def mail=(arg)
100 write_attribute(:mail, arg.to_s.strip) 115 write_attribute(:mail, arg.to_s.strip)
128 if user.auth_source 143 if user.auth_source
129 # user has an external authentication method 144 # user has an external authentication method
130 return nil unless user.auth_source.authenticate(login, password) 145 return nil unless user.auth_source.authenticate(login, password)
131 else 146 else
132 # authentication with local password 147 # authentication with local password
133 return nil unless User.hash_password(password) == user.hashed_password 148 return nil unless user.check_password?(password)
134 end 149 end
135 else 150 else
136 # user is not yet registered, try to authenticate with available sources 151 # user is not yet registered, try to authenticate with available sources
137 attrs = AuthSource.authenticate(login, password) 152 attrs = AuthSource.authenticate(login, password)
138 if attrs 153 if attrs
207 222
208 def lock! 223 def lock!
209 update_attribute(:status, STATUS_LOCKED) 224 update_attribute(:status, STATUS_LOCKED)
210 end 225 end
211 226
227 # Returns true if +clear_password+ is the correct user's password, otherwise false
212 def check_password?(clear_password) 228 def check_password?(clear_password)
213 if auth_source_id.present? 229 if auth_source_id.present?
214 auth_source.authenticate(self.login, clear_password) 230 auth_source.authenticate(self.login, clear_password)
215 else 231 else
216 User.hash_password(clear_password) == self.hashed_password 232 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
217 end 233 end
234 end
235
236 # Generates a random salt and computes hashed_password for +clear_password+
237 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
238 def salt_password(clear_password)
239 self.salt = User.generate_salt
240 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
218 end 241 end
219 242
220 # Does the backend storage allow this user to change their password? 243 # Does the backend storage allow this user to change their password?
221 def change_password_allowed? 244 def change_password_allowed?
222 return true if auth_source_id.blank? 245 return true if auth_source_id.blank?
269 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty? 292 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
270 @notified_projects_ids = nil 293 @notified_projects_ids = nil
271 notified_projects_ids 294 notified_projects_ids
272 end 295 end
273 296
297 def valid_notification_options
298 self.class.valid_notification_options(self)
299 end
300
274 # Only users that belong to more than 1 project can select projects for which they are notified 301 # Only users that belong to more than 1 project can select projects for which they are notified
275 def valid_notification_options 302 def self.valid_notification_options(user=nil)
276 # Note that @user.membership.size would fail since AR ignores 303 # Note that @user.membership.size would fail since AR ignores
277 # :include association option when doing a count 304 # :include association option when doing a count
278 if memberships.length < 1 305 if user.nil? || user.memberships.length < 1
279 MAIL_NOTIFICATION_OPTIONS.delete_if {|option| option.first == :selected} 306 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
280 else 307 else
281 MAIL_NOTIFICATION_OPTIONS 308 MAIL_NOTIFICATION_OPTIONS
282 end 309 end
283 end 310 end
284 311
354 # Return true if the user is a member of project 381 # Return true if the user is a member of project
355 def member_of?(project) 382 def member_of?(project)
356 !roles_for_project(project).detect {|role| role.member?}.nil? 383 !roles_for_project(project).detect {|role| role.member?}.nil?
357 end 384 end
358 385
386 # Returns a hash of user's projects grouped by roles
387 def projects_by_role
388 return @projects_by_role if @projects_by_role
389
390 @projects_by_role = Hash.new {|h,k| h[k]=[]}
391 memberships.each do |membership|
392 membership.roles.each do |role|
393 @projects_by_role[role] << membership.project if membership.project
394 end
395 end
396 @projects_by_role.each do |role, projects|
397 projects.uniq!
398 end
399
400 @projects_by_role
401 end
402
359 # Return true if the user is allowed to do the specified action on a specific context 403 # Return true if the user is allowed to do the specified action on a specific context
360 # Action can be: 404 # Action can be:
361 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit') 405 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
362 # * a permission Symbol (eg. :edit_project) 406 # * a permission Symbol (eg. :edit_project)
363 # Context can be: 407 # Context can be:
364 # * a project : returns true if user is allowed to do the specified action on this project 408 # * a project : returns true if user is allowed to do the specified action on this project
365 # * a group of projects : returns true if user is allowed on every project 409 # * an array of projects : returns true if user is allowed on every project
366 # * nil with options[:global] set : check if user has at least one role allowed for this action, 410 # * nil with options[:global] set : check if user has at least one role allowed for this action,
367 # or falls back to Non Member / Anonymous permissions depending if the user is logged 411 # or falls back to Non Member / Anonymous permissions depending if the user is logged
368 def allowed_to?(action, context, options={}) 412 def allowed_to?(action, context, options={}, &block)
369 if context && context.is_a?(Project) 413 if context && context.is_a?(Project)
370 # No action allowed on archived projects 414 # No action allowed on archived projects
371 return false unless context.active? 415 return false unless context.active?
372 # No action allowed on disabled modules 416 # No action allowed on disabled modules
373 return false unless context.allows_to?(action) 417 return false unless context.allows_to?(action)
374 # Admin users are authorized for anything else 418 # Admin users are authorized for anything else
375 return true if admin? 419 return true if admin?
376 420
377 roles = roles_for_project(context) 421 roles = roles_for_project(context)
378 return false unless roles 422 return false unless roles
379 roles.detect {|role| (context.is_public? || role.member?) && role.allowed_to?(action)} 423 roles.detect {|role|
380 424 (context.is_public? || role.member?) &&
425 role.allowed_to?(action) &&
426 (block_given? ? yield(role, self) : true)
427 }
381 elsif context && context.is_a?(Array) 428 elsif context && context.is_a?(Array)
382 # Authorize if user is authorized on every element of the array 429 # Authorize if user is authorized on every element of the array
383 context.map do |project| 430 context.map do |project|
384 allowed_to?(action,project,options) 431 allowed_to?(action, project, options, &block)
385 end.inject do |memo,allowed| 432 end.inject do |memo,allowed|
386 memo && allowed 433 memo && allowed
387 end 434 end
388 elsif options[:global] 435 elsif options[:global]
389 # Admin users are always authorized 436 # Admin users are always authorized
390 return true if admin? 437 return true if admin?
391 438
392 # authorize if user has at least one role that has this permission 439 # authorize if user has at least one role that has this permission
393 roles = memberships.collect {|m| m.roles}.flatten.uniq 440 roles = memberships.collect {|m| m.roles}.flatten.uniq
394 roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action)) 441 roles << (self.logged? ? Role.non_member : Role.anonymous)
442 roles.detect {|role|
443 role.allowed_to?(action) &&
444 (block_given? ? yield(role, self) : true)
445 }
395 else 446 else
396 false 447 false
397 end 448 end
398 end 449 end
399 450
400 # Is the user allowed to do the specified action on any project? 451 # Is the user allowed to do the specified action on any project?
401 # See allowed_to? for the actions and valid options. 452 # See allowed_to? for the actions and valid options.
402 def allowed_to_globally?(action, options) 453 def allowed_to_globally?(action, options, &block)
403 allowed_to?(action, nil, options.reverse_merge(:global => true)) 454 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
404 end 455 end
456
457 safe_attributes 'login',
458 'firstname',
459 'lastname',
460 'mail',
461 'mail_notification',
462 'language',
463 'custom_field_values',
464 'custom_fields',
465 'identity_url'
466
467 safe_attributes 'status',
468 'auth_source_id',
469 :if => lambda {|user, current_user| current_user.admin?}
470
471 safe_attributes 'group_ids',
472 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
405 473
406 # Utility method to help check if a user should be notified about an 474 # Utility method to help check if a user should be notified about an
407 # event. 475 # event.
408 # 476 #
409 # TODO: only supports Issue events currently 477 # TODO: only supports Issue events currently
410 def notify_about?(object) 478 def notify_about?(object)
411 case mail_notification.to_sym 479 case mail_notification
412 when :all 480 when 'all'
413 true 481 true
414 when :selected 482 when 'selected'
415 # Handled by the Project 483 # user receives notifications for created/assigned issues on unselected projects
416 when :none
417 false
418 when :only_my_events
419 if object.is_a?(Issue) && (object.author == self || object.assigned_to == self) 484 if object.is_a?(Issue) && (object.author == self || object.assigned_to == self)
420 true 485 true
421 else 486 else
422 false 487 false
423 end 488 end
424 when :only_assigned 489 when 'none'
490 false
491 when 'only_my_events'
492 if object.is_a?(Issue) && (object.author == self || object.assigned_to == self)
493 true
494 else
495 false
496 end
497 when 'only_assigned'
425 if object.is_a?(Issue) && object.assigned_to == self 498 if object.is_a?(Issue) && object.assigned_to == self
426 true 499 true
427 else 500 else
428 false 501 false
429 end 502 end
430 when :only_owner 503 when 'only_owner'
431 if object.is_a?(Issue) && object.author == self 504 if object.is_a?(Issue) && object.author == self
432 true 505 true
433 else 506 else
434 false 507 false
435 end 508 end
454 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0) 527 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
455 raise 'Unable to create the anonymous user.' if anonymous_user.new_record? 528 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
456 end 529 end
457 anonymous_user 530 anonymous_user
458 end 531 end
532
533 # Salts all existing unsalted passwords
534 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
535 # This method is used in the SaltPasswords migration and is to be kept as is
536 def self.salt_unsalted_passwords!
537 transaction do
538 User.find_each(:conditions => "salt IS NULL OR salt = ''") do |user|
539 next if user.hashed_password.blank?
540 salt = User.generate_salt
541 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
542 User.update_all("salt = '#{salt}', hashed_password = '#{hashed_password}'", ["id = ?", user.id] )
543 end
544 end
545 end
459 546
460 protected 547 protected
461 548
462 def validate 549 def validate
463 # Password length validation based on setting 550 # Password length validation based on setting
465 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i) 552 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
466 end 553 end
467 end 554 end
468 555
469 private 556 private
557
558 # Removes references that are not handled by associations
559 # Things that are not deleted are reassociated with the anonymous user
560 def remove_references_before_destroy
561 return if self.id.nil?
562
563 substitute = User.anonymous
564 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
565 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
566 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
567 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
568 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
569 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
570 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
571 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
572 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
573 # Remove private queries and keep public ones
574 Query.delete_all ['user_id = ? AND is_public = ?', id, false]
575 Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
576 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
577 Token.delete_all ['user_id = ?', id]
578 Watcher.delete_all ['user_id = ?', id]
579 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
580 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
581 end
470 582
471 # Return password digest 583 # Return password digest
472 def self.hash_password(clear_password) 584 def self.hash_password(clear_password)
473 Digest::SHA1.hexdigest(clear_password || "") 585 Digest::SHA1.hexdigest(clear_password || "")
474 end 586 end
587
588 # Returns a 128bits random salt as a hex string (32 chars long)
589 def self.generate_salt
590 ActiveSupport::SecureRandom.hex(16)
591 end
592
475 end 593 end
476 594
477 class AnonymousUser < User 595 class AnonymousUser < User
478 596
479 def validate_on_create 597 def validate_on_create
490 def admin; false end 608 def admin; false end
491 def name(*args); I18n.t(:label_user_anonymous) end 609 def name(*args); I18n.t(:label_user_anonymous) end
492 def mail; nil end 610 def mail; nil end
493 def time_zone; nil end 611 def time_zone; nil end
494 def rss_key; nil end 612 def rss_key; nil end
613
614 # Anonymous user can not be destroyed
615 def destroy
616 false
617 end
495 end 618 end