comparison app/models/.svn/text-base/user.rb.svn-base @ 511:107d36338b70 live

Merge from branch "cannam"
author Chris Cannam
date Thu, 14 Jul 2011 10:43:07 +0100
parents 753f1380d6bc
children
comparison
equal deleted inserted replaced
451:a9f6345cb43d 511:107d36338b70
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 # Active non-anonymous users scope 55 # Active non-anonymous users scope
55 named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}" 56 named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
56 57
57 acts_as_customizable 58 acts_as_customizable
58 59
59 attr_accessor :password, :password_confirmation 60 attr_accessor :password, :password_confirmation
60 attr_accessor :last_before_login_on 61 attr_accessor :last_before_login_on
61 # Prevents unauthorized assignments 62 # Prevents unauthorized assignments
62 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password, :group_ids 63 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
63 64
64 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) } 65 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
65 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }, :case_sensitive => false 66 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }, :case_sensitive => false
66 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false 67 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
67 # Login must contain lettres, numbers, underscores only 68 # Login must contain lettres, numbers, underscores only
68 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i 69 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
69 validates_length_of :login, :maximum => 30 70 validates_length_of :login, :maximum => 30
70 validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-\.]*$/i
71 validates_length_of :firstname, :lastname, :maximum => 30 71 validates_length_of :firstname, :lastname, :maximum => 30
72 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true 72 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
73 validates_length_of :mail, :maximum => 60, :allow_nil => true 73 validates_length_of :mail, :maximum => 60, :allow_nil => true
74 validates_confirmation_of :password, :allow_nil => true 74 validates_confirmation_of :password, :allow_nil => true
75 75 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
76
77 before_destroy :remove_references_before_destroy
78
79 named_scope :in_group, lambda {|group|
80 group_id = group.is_a?(Group) ? group.id : group.to_i
81 { :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] }
82 }
83 named_scope :not_in_group, lambda {|group|
84 group_id = group.is_a?(Group) ? group.id : group.to_i
85 { :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] }
86 }
87
76 def before_create 88 def before_create
77 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank? 89 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
78 true 90 true
79 end 91 end
80 92
81 def before_save 93 def before_save
82 # update hashed_password if password was set 94 # update hashed_password if password was set
83 self.hashed_password = User.hash_password(self.password) if self.password && self.auth_source_id.blank? 95 if self.password && self.auth_source_id.blank?
96 salt_password(password)
97 end
84 end 98 end
85 99
86 def reload(*args) 100 def reload(*args)
87 @name = nil 101 @name = nil
102 @projects_by_role = nil
88 super 103 super
89 end 104 end
90 105
91 def mail=(arg) 106 def mail=(arg)
92 write_attribute(:mail, arg.to_s.strip) 107 write_attribute(:mail, arg.to_s.strip)
116 if user.auth_source 131 if user.auth_source
117 # user has an external authentication method 132 # user has an external authentication method
118 return nil unless user.auth_source.authenticate(login, password) 133 return nil unless user.auth_source.authenticate(login, password)
119 else 134 else
120 # authentication with local password 135 # authentication with local password
121 return nil unless User.hash_password(password) == user.hashed_password 136 return nil unless user.check_password?(password)
122 end 137 end
123 else 138 else
124 # user is not yet registered, try to authenticate with available sources 139 # user is not yet registered, try to authenticate with available sources
125 attrs = AuthSource.authenticate(login, password) 140 attrs = AuthSource.authenticate(login, password)
126 if attrs 141 if attrs
195 210
196 def lock! 211 def lock!
197 update_attribute(:status, STATUS_LOCKED) 212 update_attribute(:status, STATUS_LOCKED)
198 end 213 end
199 214
215 # Returns true if +clear_password+ is the correct user's password, otherwise false
200 def check_password?(clear_password) 216 def check_password?(clear_password)
201 if auth_source_id.present? 217 if auth_source_id.present?
202 auth_source.authenticate(self.login, clear_password) 218 auth_source.authenticate(self.login, clear_password)
203 else 219 else
204 User.hash_password(clear_password) == self.hashed_password 220 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
205 end 221 end
222 end
223
224 # Generates a random salt and computes hashed_password for +clear_password+
225 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
226 def salt_password(clear_password)
227 self.salt = User.generate_salt
228 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
206 end 229 end
207 230
208 # Does the backend storage allow this user to change their password? 231 # Does the backend storage allow this user to change their password?
209 def change_password_allowed? 232 def change_password_allowed?
210 return true if auth_source_id.blank? 233 return true if auth_source_id.blank?
257 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty? 280 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
258 @notified_projects_ids = nil 281 @notified_projects_ids = nil
259 notified_projects_ids 282 notified_projects_ids
260 end 283 end
261 284
285 def valid_notification_options
286 self.class.valid_notification_options(self)
287 end
288
262 # Only users that belong to more than 1 project can select projects for which they are notified 289 # Only users that belong to more than 1 project can select projects for which they are notified
263 def valid_notification_options 290 def self.valid_notification_options(user=nil)
264 # Note that @user.membership.size would fail since AR ignores 291 # Note that @user.membership.size would fail since AR ignores
265 # :include association option when doing a count 292 # :include association option when doing a count
266 if memberships.length < 1 293 if user.nil? || user.memberships.length < 1
267 MAIL_NOTIFICATION_OPTIONS.delete_if {|option| option.first == :selected} 294 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
268 else 295 else
269 MAIL_NOTIFICATION_OPTIONS 296 MAIL_NOTIFICATION_OPTIONS
270 end 297 end
271 end 298 end
272 299
342 # Return true if the user is a member of project 369 # Return true if the user is a member of project
343 def member_of?(project) 370 def member_of?(project)
344 !roles_for_project(project).detect {|role| role.member?}.nil? 371 !roles_for_project(project).detect {|role| role.member?}.nil?
345 end 372 end
346 373
374 # Returns a hash of user's projects grouped by roles
375 def projects_by_role
376 return @projects_by_role if @projects_by_role
377
378 @projects_by_role = Hash.new {|h,k| h[k]=[]}
379 memberships.each do |membership|
380 membership.roles.each do |role|
381 @projects_by_role[role] << membership.project if membership.project
382 end
383 end
384 @projects_by_role.each do |role, projects|
385 projects.uniq!
386 end
387
388 @projects_by_role
389 end
390
347 # Return true if the user is allowed to do the specified action on a specific context 391 # Return true if the user is allowed to do the specified action on a specific context
348 # Action can be: 392 # Action can be:
349 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit') 393 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
350 # * a permission Symbol (eg. :edit_project) 394 # * a permission Symbol (eg. :edit_project)
351 # Context can be: 395 # Context can be:
352 # * a project : returns true if user is allowed to do the specified action on this project 396 # * a project : returns true if user is allowed to do the specified action on this project
353 # * a group of projects : returns true if user is allowed on every project 397 # * an array of projects : returns true if user is allowed on every project
354 # * nil with options[:global] set : check if user has at least one role allowed for this action, 398 # * nil with options[:global] set : check if user has at least one role allowed for this action,
355 # or falls back to Non Member / Anonymous permissions depending if the user is logged 399 # or falls back to Non Member / Anonymous permissions depending if the user is logged
356 def allowed_to?(action, context, options={}) 400 def allowed_to?(action, context, options={}, &block)
357 if context && context.is_a?(Project) 401 if context && context.is_a?(Project)
358 # No action allowed on archived projects 402 # No action allowed on archived projects
359 return false unless context.active? 403 return false unless context.active?
360 # No action allowed on disabled modules 404 # No action allowed on disabled modules
361 return false unless context.allows_to?(action) 405 return false unless context.allows_to?(action)
362 # Admin users are authorized for anything else 406 # Admin users are authorized for anything else
363 return true if admin? 407 return true if admin?
364 408
365 roles = roles_for_project(context) 409 roles = roles_for_project(context)
366 return false unless roles 410 return false unless roles
367 roles.detect {|role| (context.is_public? || role.member?) && role.allowed_to?(action)} 411 roles.detect {|role|
368 412 (context.is_public? || role.member?) &&
413 role.allowed_to?(action) &&
414 (block_given? ? yield(role, self) : true)
415 }
369 elsif context && context.is_a?(Array) 416 elsif context && context.is_a?(Array)
370 # Authorize if user is authorized on every element of the array 417 # Authorize if user is authorized on every element of the array
371 context.map do |project| 418 context.map do |project|
372 allowed_to?(action,project,options) 419 allowed_to?(action, project, options, &block)
373 end.inject do |memo,allowed| 420 end.inject do |memo,allowed|
374 memo && allowed 421 memo && allowed
375 end 422 end
376 elsif options[:global] 423 elsif options[:global]
377 # Admin users are always authorized 424 # Admin users are always authorized
378 return true if admin? 425 return true if admin?
379 426
380 # authorize if user has at least one role that has this permission 427 # authorize if user has at least one role that has this permission
381 roles = memberships.collect {|m| m.roles}.flatten.uniq 428 roles = memberships.collect {|m| m.roles}.flatten.uniq
382 roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action)) 429 roles << (self.logged? ? Role.non_member : Role.anonymous)
430 roles.detect {|role|
431 role.allowed_to?(action) &&
432 (block_given? ? yield(role, self) : true)
433 }
383 else 434 else
384 false 435 false
385 end 436 end
386 end 437 end
387 438
388 # Is the user allowed to do the specified action on any project? 439 # Is the user allowed to do the specified action on any project?
389 # See allowed_to? for the actions and valid options. 440 # See allowed_to? for the actions and valid options.
390 def allowed_to_globally?(action, options) 441 def allowed_to_globally?(action, options, &block)
391 allowed_to?(action, nil, options.reverse_merge(:global => true)) 442 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
392 end 443 end
444
445 safe_attributes 'login',
446 'firstname',
447 'lastname',
448 'mail',
449 'mail_notification',
450 'language',
451 'custom_field_values',
452 'custom_fields',
453 'identity_url'
454
455 safe_attributes 'status',
456 'auth_source_id',
457 :if => lambda {|user, current_user| current_user.admin?}
458
459 safe_attributes 'group_ids',
460 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
393 461
394 # Utility method to help check if a user should be notified about an 462 # Utility method to help check if a user should be notified about an
395 # event. 463 # event.
396 # 464 #
397 # TODO: only supports Issue events currently 465 # TODO: only supports Issue events currently
398 def notify_about?(object) 466 def notify_about?(object)
399 case mail_notification.to_sym 467 case mail_notification
400 when :all 468 when 'all'
401 true 469 true
402 when :selected 470 when 'selected'
403 # Handled by the Project 471 # user receives notifications for created/assigned issues on unselected projects
404 when :none
405 false
406 when :only_my_events
407 if object.is_a?(Issue) && (object.author == self || object.assigned_to == self) 472 if object.is_a?(Issue) && (object.author == self || object.assigned_to == self)
408 true 473 true
409 else 474 else
410 false 475 false
411 end 476 end
412 when :only_assigned 477 when 'none'
478 false
479 when 'only_my_events'
480 if object.is_a?(Issue) && (object.author == self || object.assigned_to == self)
481 true
482 else
483 false
484 end
485 when 'only_assigned'
413 if object.is_a?(Issue) && object.assigned_to == self 486 if object.is_a?(Issue) && object.assigned_to == self
414 true 487 true
415 else 488 else
416 false 489 false
417 end 490 end
418 when :only_owner 491 when 'only_owner'
419 if object.is_a?(Issue) && object.author == self 492 if object.is_a?(Issue) && object.author == self
420 true 493 true
421 else 494 else
422 false 495 false
423 end 496 end
442 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0) 515 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
443 raise 'Unable to create the anonymous user.' if anonymous_user.new_record? 516 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
444 end 517 end
445 anonymous_user 518 anonymous_user
446 end 519 end
520
521 # Salts all existing unsalted passwords
522 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
523 # This method is used in the SaltPasswords migration and is to be kept as is
524 def self.salt_unsalted_passwords!
525 transaction do
526 User.find_each(:conditions => "salt IS NULL OR salt = ''") do |user|
527 next if user.hashed_password.blank?
528 salt = User.generate_salt
529 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
530 User.update_all("salt = '#{salt}', hashed_password = '#{hashed_password}'", ["id = ?", user.id] )
531 end
532 end
533 end
447 534
448 protected 535 protected
449 536
450 def validate 537 def validate
451 # Password length validation based on setting 538 # Password length validation based on setting
453 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i) 540 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
454 end 541 end
455 end 542 end
456 543
457 private 544 private
545
546 # Removes references that are not handled by associations
547 # Things that are not deleted are reassociated with the anonymous user
548 def remove_references_before_destroy
549 return if self.id.nil?
550
551 substitute = User.anonymous
552 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
553 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
554 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
555 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
556 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
557 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
558 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
559 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
560 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
561 # Remove private queries and keep public ones
562 Query.delete_all ['user_id = ? AND is_public = ?', id, false]
563 Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
564 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
565 Token.delete_all ['user_id = ?', id]
566 Watcher.delete_all ['user_id = ?', id]
567 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
568 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
569 end
458 570
459 # Return password digest 571 # Return password digest
460 def self.hash_password(clear_password) 572 def self.hash_password(clear_password)
461 Digest::SHA1.hexdigest(clear_password || "") 573 Digest::SHA1.hexdigest(clear_password || "")
462 end 574 end
575
576 # Returns a 128bits random salt as a hex string (32 chars long)
577 def self.generate_salt
578 ActiveSupport::SecureRandom.hex(16)
579 end
580
463 end 581 end
464 582
465 class AnonymousUser < User 583 class AnonymousUser < User
466 584
467 def validate_on_create 585 def validate_on_create
478 def admin; false end 596 def admin; false end
479 def name(*args); I18n.t(:label_user_anonymous) end 597 def name(*args); I18n.t(:label_user_anonymous) end
480 def mail; nil end 598 def mail; nil end
481 def time_zone; nil end 599 def time_zone; nil end
482 def rss_key; nil end 600 def rss_key; nil end
601
602 # Anonymous user can not be destroyed
603 def destroy
604 false
605 end
483 end 606 end