To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.

Statistics Download as Zip
| Branch: | Tag: | Revision:

root / app / models / user.rb @ 443:350acce374a2

History | View | Annotate | Download (20.3 KB)

1
# Redmine - project management software
2
# Copyright (C) 2006-2011  Jean-Philippe Lang
3
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8
# 
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
# 
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17

    
18
require "digest/sha1"
19

    
20
class User < Principal
21
  include Redmine::SafeAttributes
22
  
23
  # Account statuses
24
  STATUS_ANONYMOUS  = 0
25
  STATUS_ACTIVE     = 1
26
  STATUS_REGISTERED = 2
27
  STATUS_LOCKED     = 3
28
  
29
  USER_FORMATS = {
30
    :firstname_lastname => '#{firstname} #{lastname}',
31
    :firstname => '#{firstname}',
32
    :lastname_firstname => '#{lastname} #{firstname}',
33
    :lastname_coma_firstname => '#{lastname}, #{firstname}',
34
    :username => '#{login}'
35
  }
36

    
37
  MAIL_NOTIFICATION_OPTIONS = [
38
    ['all', :label_user_mail_option_all],
39
    ['selected', :label_user_mail_option_selected],
40
    ['only_my_events', :label_user_mail_option_only_my_events],
41
    ['only_assigned', :label_user_mail_option_only_assigned],
42
    ['only_owner', :label_user_mail_option_only_owner],
43
    ['none', :label_user_mail_option_none]
44
  ]
45

    
46
  has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
47
                                   :after_remove => Proc.new {|user, group| group.user_removed(user)}
48
  has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
49
  has_many :changesets, :dependent => :nullify
50
  has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
51
  has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
52
  has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
53
  belongs_to :auth_source
54
  
55
  has_one :ssamr_user_detail, :dependent => :destroy, :class_name => 'SsamrUserDetail'
56
  accepts_nested_attributes_for :ssamr_user_detail
57

    
58
  # Active non-anonymous users scope
59
  named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
60
  
61
  acts_as_customizable
62
  
63
  attr_accessor :password, :password_confirmation
64
  attr_accessor :last_before_login_on
65
  # Prevents unauthorized assignments
66
  attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
67
        
68
  validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
69
  
70
  # TODO: is this validation correct validates_presence_of :ssamr_user_detail
71
  
72
  validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }, :case_sensitive => false
73
  validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
74
  # Login must contain lettres, numbers, underscores only
75
  validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
76
  validates_length_of :login, :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
79
  validates_length_of :mail, :maximum => 60, :allow_nil => true
80
  validates_confirmation_of :password, :allow_nil => true
81
  validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
82

    
83
  before_destroy :remove_references_before_destroy
84
  
85
  validates_acceptance_of :terms_and_conditions, :on => :create, :message => :must_accept_terms_and_conditions
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
  
96
  def before_create
97
    self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
98
    true
99
  end
100
  
101
  def before_save
102
    # update hashed_password if password was set
103
    if self.password && self.auth_source_id.blank?
104
      salt_password(password)
105
    end
106
  end
107
  
108
  def reload(*args)
109
    @name = nil
110
    @projects_by_role = nil
111
    super
112
  end
113
  
114
  def mail=(arg)
115
    write_attribute(:mail, arg.to_s.strip)
116
  end
117
  
118
  def description=(arg)
119
    write_attribute(:description, arg.to_s.strip)
120
  end
121
    
122
  def identity_url=(url)
123
    if url.blank?
124
      write_attribute(:identity_url, '')
125
    else
126
      begin
127
        write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
128
      rescue OpenIdAuthentication::InvalidOpenId
129
        # Invlaid url, don't save
130
      end
131
    end
132
    self.read_attribute(:identity_url)
133
  end
134
  
135
  # Returns the user that matches provided login and password, or nil
136
  def self.try_to_login(login, password)
137
    # Make sure no one can sign in with an empty password
138
    return nil if password.to_s.empty?
139
    user = find_by_login(login)
140
    if user
141
      # user is already in local database
142
      return nil if !user.active?
143
      if user.auth_source
144
        # user has an external authentication method
145
        return nil unless user.auth_source.authenticate(login, password)
146
      else
147
        # authentication with local password
148
        return nil unless user.check_password?(password)
149
      end
150
    else
151
      # user is not yet registered, try to authenticate with available sources
152
      attrs = AuthSource.authenticate(login, password)
153
      if attrs
154
        user = new(attrs)
155
        user.login = login
156
        user.language = Setting.default_language
157
        if user.save
158
          user.reload
159
          logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
160
        end
161
      end
162
    end    
163
    user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
164
    user
165
  rescue => text
166
    raise text
167
  end
168
  
169
  # Returns the user who matches the given autologin +key+ or nil
170
  def self.try_to_autologin(key)
171
    tokens = Token.find_all_by_action_and_value('autologin', key)
172
    # Make sure there's only 1 token that matches the key
173
    if tokens.size == 1
174
      token = tokens.first
175
      if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
176
        token.user.update_attribute(:last_login_on, Time.now)
177
        token.user
178
      end
179
    end
180
  end
181
        
182
  # Return user's full name for display
183
  def name(formatter = nil)
184
    if formatter
185
      eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
186
    else
187
      @name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
188
    end
189
  end
190
  
191
  def active?
192
    self.status == STATUS_ACTIVE
193
  end
194

    
195
  def registered?
196
    self.status == STATUS_REGISTERED
197
  end
198
    
199
  def locked?
200
    self.status == STATUS_LOCKED
201
  end
202

    
203
  def activate
204
    self.status = STATUS_ACTIVE
205
  end
206

    
207
  def register
208
    self.status = STATUS_REGISTERED
209
  end
210

    
211
  def lock
212
    self.status = STATUS_LOCKED
213
  end
214

    
215
  def activate!
216
    update_attribute(:status, STATUS_ACTIVE)
217
  end
218

    
219
  def register!
220
    update_attribute(:status, STATUS_REGISTERED)
221
  end
222

    
223
  def lock!
224
    update_attribute(:status, STATUS_LOCKED)
225
  end
226

    
227
  # Returns true if +clear_password+ is the correct user's password, otherwise false
228
  def check_password?(clear_password)
229
    if auth_source_id.present?
230
      auth_source.authenticate(self.login, clear_password)
231
    else
232
      User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
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}")
241
  end
242

    
243
  # Does the backend storage allow this user to change their password?
244
  def change_password_allowed?
245
    return true if auth_source_id.blank?
246
    return auth_source.allow_password_changes?
247
  end
248

    
249
  # Generate and set a random password.  Useful for automated user creation
250
  # Based on Token#generate_token_value
251
  #
252
  def random_password
253
    chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
254
    password = ''
255
    40.times { |i| password << chars[rand(chars.size-1)] }
256
    self.password = password
257
    self.password_confirmation = password
258
    self
259
  end
260
  
261
  def pref
262
    self.preference ||= UserPreference.new(:user => self)
263
  end
264
  
265
  def time_zone
266
    @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
267
  end
268
  
269
  def wants_comments_in_reverse_order?
270
    self.pref[:comments_sorting] == 'desc'
271
  end
272
  
273
  # Return user's RSS key (a 40 chars long string), used to access feeds
274
  def rss_key
275
    token = self.rss_token || Token.create(:user => self, :action => 'feeds')
276
    token.value
277
  end
278

    
279
  # Return user's API key (a 40 chars long string), used to access the API
280
  def api_key
281
    token = self.api_token || self.create_api_token(:action => 'api')
282
    token.value
283
  end
284
  
285
  # Return an array of project ids for which the user has explicitly turned mail notifications on
286
  def notified_projects_ids
287
    @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
288
  end
289
  
290
  def notified_project_ids=(ids)
291
    Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
292
    Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
293
    @notified_projects_ids = nil
294
    notified_projects_ids
295
  end
296

    
297
  def valid_notification_options
298
    self.class.valid_notification_options(self)
299
  end
300

    
301
  # Only users that belong to more than 1 project can select projects for which they are notified
302
  def self.valid_notification_options(user=nil)
303
    # Note that @user.membership.size would fail since AR ignores
304
    # :include association option when doing a count
305
    if user.nil? || user.memberships.length < 1
306
      MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
307
    else
308
      MAIL_NOTIFICATION_OPTIONS
309
    end
310
  end
311

    
312
  # Find a user account by matching the exact login and then a case-insensitive
313
  # version.  Exact matches will be given priority.
314
  def self.find_by_login(login)
315
    # force string comparison to be case sensitive on MySQL
316
    type_cast = (ActiveRecord::Base.connection.adapter_name == 'MySQL') ? 'BINARY' : ''
317
    
318
    # First look for an exact match
319
    user = first(:conditions => ["#{type_cast} login = ?", login])
320
    # Fail over to case-insensitive if none was found
321
    user ||= first(:conditions => ["#{type_cast} LOWER(login) = ?", login.to_s.downcase])
322
  end
323

    
324
  def self.find_by_rss_key(key)
325
    token = Token.find_by_value(key)
326
    token && token.user.active? ? token.user : nil
327
  end
328
  
329
  def self.find_by_api_key(key)
330
    token = Token.find_by_action_and_value('api', key)
331
    token && token.user.active? ? token.user : nil
332
  end
333
  
334
  # Makes find_by_mail case-insensitive
335
  def self.find_by_mail(mail)
336
    find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
337
  end
338
  
339
  def to_s
340
    name
341
  end
342
  
343
  # Returns the current day according to user's time zone
344
  def today
345
    if time_zone.nil?
346
      Date.today
347
    else
348
      Time.now.in_time_zone(time_zone).to_date
349
    end
350
  end
351
  
352
  def logged?
353
    true
354
  end
355
  
356
  def anonymous?
357
    !logged?
358
  end
359
  
360
  # Return user's roles for project
361
  def roles_for_project(project)
362
    roles = []
363
    # No role on archived projects
364
    return roles unless project && project.active?
365
    if logged?
366
      # Find project membership
367
      membership = memberships.detect {|m| m.project_id == project.id}
368
      if membership
369
        roles = membership.roles
370
      else
371
        @role_non_member ||= Role.non_member
372
        roles << @role_non_member
373
      end
374
    else
375
      @role_anonymous ||= Role.anonymous
376
      roles << @role_anonymous
377
    end
378
    roles
379
  end
380
  
381
  # Return true if the user is a member of project
382
  def member_of?(project)
383
    !roles_for_project(project).detect {|role| role.member?}.nil?
384
  end
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
  
403
  # Return true if the user is allowed to do the specified action on a specific context
404
  # Action can be:
405
  # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
406
  # * a permission Symbol (eg. :edit_project)
407
  # Context can be:
408
  # * a project : returns true if user is allowed to do the specified action on this project
409
  # * an array of projects : returns true if user is allowed on every project
410
  # * nil with options[:global] set : check if user has at least one role allowed for this action, 
411
  #   or falls back to Non Member / Anonymous permissions depending if the user is logged
412
  def allowed_to?(action, context, options={}, &block)
413
    if context && context.is_a?(Project)
414
      # No action allowed on archived projects
415
      return false unless context.active?
416
      # No action allowed on disabled modules
417
      return false unless context.allows_to?(action)
418
      # Admin users are authorized for anything else
419
      return true if admin?
420
      
421
      roles = roles_for_project(context)
422
      return false unless roles
423
      roles.detect {|role|
424
        (context.is_public? || role.member?) &&
425
        role.allowed_to?(action) &&
426
        (block_given? ? yield(role, self) : true)
427
      }
428
    elsif context && context.is_a?(Array)
429
      # Authorize if user is authorized on every element of the array
430
      context.map do |project|
431
        allowed_to?(action, project, options, &block)
432
      end.inject do |memo,allowed|
433
        memo && allowed
434
      end
435
    elsif options[:global]
436
      # Admin users are always authorized
437
      return true if admin?
438
      
439
      # authorize if user has at least one role that has this permission
440
      roles = memberships.collect {|m| m.roles}.flatten.uniq
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
      }
446
    else
447
      false
448
    end
449
  end
450

    
451
  # Is the user allowed to do the specified action on any project?
452
  # See allowed_to? for the actions and valid options.
453
  def allowed_to_globally?(action, options, &block)
454
    allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
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?}
473
  
474
  # Utility method to help check if a user should be notified about an
475
  # event.
476
  #
477
  # TODO: only supports Issue events currently
478
  def notify_about?(object)
479
    case mail_notification
480
    when 'all'
481
      true
482
    when 'selected'
483
      # user receives notifications for created/assigned issues on unselected projects
484
      if object.is_a?(Issue) && (object.author == self || object.assigned_to == self)
485
        true
486
      else
487
        false
488
      end
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'
498
      if object.is_a?(Issue) && object.assigned_to == self
499
        true
500
      else
501
        false
502
      end
503
    when 'only_owner'
504
      if object.is_a?(Issue) && object.author == self
505
        true
506
      else
507
        false
508
      end
509
    else
510
      false
511
    end
512
  end
513
  
514
  def self.current=(user)
515
    @current_user = user
516
  end
517
  
518
  def self.current
519
    @current_user ||= User.anonymous
520
  end
521
  
522
  # Returns the anonymous user.  If the anonymous user does not exist, it is created.  There can be only
523
  # one anonymous user per database.
524
  def self.anonymous
525
    anonymous_user = AnonymousUser.find(:first)
526
    if anonymous_user.nil?
527
      anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
528
      raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
529
    end
530
    anonymous_user
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
546
  
547
  protected
548
  
549
  def validate
550
    # Password length validation based on setting
551
    if !password.nil? && password.size < Setting.password_min_length.to_i
552
      errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
553
    end
554
  end
555
  
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
582
    
583
  # Return password digest
584
  def self.hash_password(clear_password)
585
    Digest::SHA1.hexdigest(clear_password || "")
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
  
593
end
594

    
595
class AnonymousUser < User
596
  
597
  def validate_on_create
598
    # There should be only one AnonymousUser in the database
599
    errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
600
  end
601
  
602
  def available_custom_fields
603
    []
604
  end
605
  
606
  # Overrides a few properties
607
  def logged?; false end
608
  def admin; false end
609
  def name(*args); I18n.t(:label_user_anonymous) end
610
  def mail; nil end
611
  def time_zone; nil end
612
  def rss_key; nil end
613
  
614
  # Anonymous user can not be destroyed
615
  def destroy
616
    false
617
  end
618
end