Mercurial > hg > soundsoftware-site
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 |