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

require "digest/sha1"

jplang's avatar
jplang committed
20
class User < Principal
21
  include Redmine::SafeAttributes
22

23
  # Different ways of displaying/sorting users
24
  USER_FORMATS = {
25 26 27 28 29
    :firstname_lastname => {
        :string => '#{firstname} #{lastname}',
        :order => %w(firstname lastname id),
        :setting_order => 1
      },
30 31 32 33 34
    :firstname_lastinitial => {
        :string => '#{firstname} #{lastname.to_s.chars.first}.',
        :order => %w(firstname lastname id),
        :setting_order => 2
      },
35 36 37 38 39
    :firstinitial_lastname => {
        :string => '#{firstname.to_s.gsub(/(([[:alpha:]])[[:alpha:]]*\.?)/, \'\2.\')} #{lastname}',
        :order => %w(firstname lastname id),
        :setting_order => 2
      },
40 41 42
    :firstname => {
        :string => '#{firstname}',
        :order => %w(firstname id),
43
        :setting_order => 3
44 45 46 47
      },
    :lastname_firstname => {
        :string => '#{lastname} #{firstname}',
        :order => %w(lastname firstname id),
48
        :setting_order => 4
49
      },
50 51 52 53 54
    :lastnamefirstname => {
        :string => '#{lastname}#{firstname}',
        :order => %w(lastname firstname id),
        :setting_order => 5
      },
jplang's avatar
jplang committed
55
    :lastname_comma_firstname => {
56 57
        :string => '#{lastname}, #{firstname}',
        :order => %w(lastname firstname id),
58
        :setting_order => 6
59
      },
60 61 62
    :lastname => {
        :string => '#{lastname}',
        :order => %w(lastname id),
63
        :setting_order => 7
64
      },
65 66 67
    :username => {
        :string => '#{login}',
        :order => %w(login id),
68
        :setting_order => 8
69
      },
70
  }
71

72
  MAIL_NOTIFICATION_OPTIONS = [
73 74 75 76 77 78 79
    ['all', :label_user_mail_option_all],
    ['selected', :label_user_mail_option_selected],
    ['only_my_events', :label_user_mail_option_only_my_events],
    ['only_assigned', :label_user_mail_option_only_assigned],
    ['only_owner', :label_user_mail_option_only_owner],
    ['none', :label_user_mail_option_none]
  ]
80

81 82 83 84
  has_and_belongs_to_many :groups,
                          :join_table   => "#{table_name_prefix}groups_users#{table_name_suffix}",
                          :after_add    => Proc.new {|user, group| group.user_added(user)},
                          :after_remove => Proc.new {|user, group| group.user_removed(user)}
85
  has_many :changesets, :dependent => :nullify
86
  has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
jplang's avatar
jplang committed
87 88
  has_one :rss_token, lambda {where "action='feeds'"}, :class_name => 'Token'
  has_one :api_token, lambda {where "action='api'"}, :class_name => 'Token'
89 90
  has_one :email_address, lambda {where :is_default => true}, :autosave => true
  has_many :email_addresses, :dependent => :delete_all
91
  belongs_to :auth_source
92

93 94
  scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
  scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
95

96
  acts_as_customizable
97

98
  attr_accessor :password, :password_confirmation, :generate_password
99
  attr_accessor :last_before_login_on
100 101
  attr_accessor :remote_ip

102 103 104
  LOGIN_LENGTH_LIMIT = 60
  MAIL_LENGTH_LIMIT = 60

105
  validates_presence_of :login, :firstname, :lastname, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
106
  validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
107
  # Login must contain letters, numbers, underscores only
108
  validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i
109
  validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
110
  validates_length_of :firstname, :lastname, :maximum => 30
111
  validates_length_of :identity_url, maximum: 255
112
  validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
113
  validate :validate_password_length
jplang's avatar
jplang committed
114 115 116 117 118
  validate do
    if password_confirmation && password != password_confirmation
      errors.add(:password, :confirmation)
    end
  end
119

120 121
  self.valid_statuses = [STATUS_ACTIVE, STATUS_REGISTERED, STATUS_LOCKED]

122
  before_validation :instantiate_email_address
123
  before_create :set_mail_notification
124
  before_save   :generate_password_if_needed, :update_hashed_password
125
  before_destroy :remove_references_before_destroy
126 127
  after_save :update_notified_project_ids, :destroy_tokens, :deliver_security_notification
  after_destroy :deliver_security_notification
128

jplang's avatar
jplang committed
129 130 131 132
  scope :admin, lambda {|*args|
    admin = args.size > 0 ? !!args.first : true
    where(:admin => admin)
  }
133
  scope :in_group, lambda {|group|
134
    group_id = group.is_a?(Group) ? group.id : group.to_i
jplang's avatar
jplang committed
135
    where("#{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)
136
  }
137
  scope :not_in_group, lambda {|group|
138
    group_id = group.is_a?(Group) ? group.id : group.to_i
jplang's avatar
jplang committed
139
    where("#{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)
140
  }
141
  scope :sorted, lambda { order(*User.fields_for_order_statement)}
142 143 144
  scope :having_mail, lambda {|arg|
    addresses = Array.wrap(arg).map {|a| a.to_s.downcase}
    if addresses.any?
jplang's avatar
jplang committed
145
      joins(:email_addresses).where("LOWER(#{EmailAddress.table_name}.address) IN (?)", addresses).distinct
146 147 148 149
    else
      none
    end
  }
150

151
  def set_mail_notification
152
    self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
153 154
    true
  end
155

156
  def update_hashed_password
157
    # update hashed_password if password was set
158 159 160
    if self.password && self.auth_source_id.blank?
      salt_password(password)
    end
161
  end
162

163
  alias :base_reload :reload
164 165
  def reload(*args)
    @name = nil
jplang's avatar
jplang committed
166
    @roles = nil
167
    @projects_by_role = nil
168
    @project_ids_by_role = nil
169
    @membership_by_project_id = nil
170 171
    @notified_projects_ids = nil
    @notified_projects_ids_changed = false
jplang's avatar
jplang committed
172
    @builtin_role = nil
173
    @visible_project_ids = nil
174
    @managed_roles = nil
175
    base_reload(*args)
176
  end
177

178 179 180 181
  def mail
    email_address.try(:address)
  end

jplang's avatar
jplang committed
182
  def mail=(arg)
183 184 185 186 187 188 189 190 191 192
    email = email_address || build_email_address
    email.address = arg
  end

  def mail_changed?
    email_address.try(:address_changed?)
  end

  def mails
    email_addresses.pluck(:address)
jplang's avatar
jplang committed
193
  end
194

jplang's avatar
jplang committed
195 196 197 198 199 200 201 202 203
  def self.find_or_initialize_by_identity_url(url)
    user = where(:identity_url => url).first
    unless user
      user = User.new
      user.identity_url = url
    end
    user
  end

204
  def identity_url=(url)
205 206 207 208 209 210
    if url.blank?
      write_attribute(:identity_url, '')
    else
      begin
        write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
      rescue OpenIdAuthentication::InvalidOpenId
211
        # Invalid url, don't save
212
      end
213 214 215
    end
    self.read_attribute(:identity_url)
  end
216

217
  # Returns the user that matches provided login and password, or nil
218
  def self.try_to_login(login, password, active_only=true)
219
    login = login.to_s.strip
220 221
    password = password.to_s

jplang's avatar
jplang committed
222 223
    # Make sure no one can sign in with an empty login or password
    return nil if login.empty? || password.empty?
jplang's avatar
jplang committed
224
    user = find_by_login(login)
225 226
    if user
      # user is already in local database
jplang's avatar
jplang committed
227
      return nil unless user.check_password?(password)
228
      return nil if !user.active? && active_only
229 230 231 232
    else
      # user is not yet registered, try to authenticate with available sources
      attrs = AuthSource.authenticate(login, password)
      if attrs
233
        user = new(attrs)
234 235 236 237
        user.login = login
        user.language = Setting.default_language
        if user.save
          user.reload
238
          logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
239 240
        end
      end
241
    end
242
    user.update_last_login_on! if user && !user.new_record? && user.active?
243
    user
244 245
  rescue => text
    raise text
246
  end
247

248 249
  # Returns the user who matches the given autologin +key+ or nil
  def self.try_to_autologin(key)
250 251
    user = Token.find_active_user('autologin', key, Setting.autologin.to_i)
    if user
252
      user.update_last_login_on!
253
      user
254 255
    end
  end
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271

  def self.name_formatter(formatter = nil)
    USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
  end

  # Returns an array of fields names than can be used to make an order statement for users
  # according to how user names are displayed
  # Examples:
  #
  #   User.fields_for_order_statement              => ['users.login', 'users.id']
  #   User.fields_for_order_statement('authors')   => ['authors.login', 'authors.id']
  def self.fields_for_order_statement(table=nil)
    table ||= table_name
    name_formatter[:order].map {|field| "#{table}.#{field}"}
  end

272
  # Return user's full name for display
273
  def name(formatter = nil)
274
    f = self.class.name_formatter(formatter)
275
    if formatter
276
      eval('"' + f[:string] + '"')
277
    else
278
      @name ||= eval('"' + f[:string] + '"')
279
    end
280
  end
281

282 283 284 285 286 287 288
  def active?
    self.status == STATUS_ACTIVE
  end

  def registered?
    self.status == STATUS_REGISTERED
  end
289

290 291 292 293
  def locked?
    self.status == STATUS_LOCKED
  end

294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317
  def activate
    self.status = STATUS_ACTIVE
  end

  def register
    self.status = STATUS_REGISTERED
  end

  def lock
    self.status = STATUS_LOCKED
  end

  def activate!
    update_attribute(:status, STATUS_ACTIVE)
  end

  def register!
    update_attribute(:status, STATUS_REGISTERED)
  end

  def lock!
    update_attribute(:status, STATUS_LOCKED)
  end

318 319 320 321 322 323
  def update_last_login_on!
    return if last_login_on.present? && last_login_on >= 1.minute.ago

    update_column(:last_login_on, Time.now)
  end

324
  # Returns true if +clear_password+ is the correct user's password, otherwise false
325
  def check_password?(clear_password)
326 327 328
    if auth_source_id.present?
      auth_source.authenticate(self.login, clear_password)
    else
329
      User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
330 331
    end
  end
332

333 334 335 336 337
  # Generates a random salt and computes hashed_password for +clear_password+
  # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
  def salt_password(clear_password)
    self.salt = User.generate_salt
    self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
338
    self.passwd_changed_on = Time.now.change(:usec => 0)
339
  end
340 341 342

  # Does the backend storage allow this user to change their password?
  def change_password_allowed?
jplang's avatar
jplang committed
343
    return true if auth_source.nil?
344
    return auth_source.allow_password_changes?
345
  end
346

jplang's avatar
jplang committed
347
  # Returns true if the user password has expired
348
  def password_expired?
jplang's avatar
jplang committed
349 350
    period = Setting.password_max_age.to_i
    if period.zero?
351 352
      false
    else
jplang's avatar
jplang committed
353
      changed_on = self.passwd_changed_on || Time.at(0)
354 355 356 357
      changed_on < period.days.ago
    end
  end

358
  def must_change_password?
359
    (must_change_passwd? || password_expired?) && change_password_allowed?
360 361
  end

362
  def generate_password?
363
    ActiveRecord::Type::Boolean.new.deserialize(generate_password)
364 365 366 367
  end

  # Generate and set a random password on given length
  def random_password(length=40)
368
    chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
369
    chars -= %w(0 O 1 l)
370
    password = ''
371
    length.times {|i| password << chars[SecureRandom.random_number(chars.size)] }
372 373 374 375
    self.password = password
    self.password_confirmation = password
    self
  end
376

377 378 379
  def pref
    self.preference ||= UserPreference.new(:user => self)
  end
380

381
  def time_zone
jplang's avatar
jplang committed
382
    @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
383
  end
384

385 386 387 388 389 390 391 392 393 394 395 396
  def force_default_language?
    Setting.force_default_language_for_loggedin?
  end

  def language
    if force_default_language?
      Setting.default_language
    else
      super
    end
  end

397 398 399
  def wants_comments_in_reverse_order?
    self.pref[:comments_sorting] == 'desc'
  end
400

jplang's avatar
jplang committed
401 402
  # Return user's RSS key (a 40 chars long string), used to access feeds
  def rss_key
403 404 405 406
    if rss_token.nil?
      create_rss_token(:action => 'feeds')
    end
    rss_token.value
407
  end
408 409 410

  # Return user's API key (a 40 chars long string), used to access the API
  def api_key
411 412 413 414
    if api_token.nil?
      create_api_token(:action => 'api')
    end
    api_token.value
415
  end
416

417 418 419 420 421 422
  # Generates a new session token and returns its value
  def generate_session_token
    token = Token.create!(:user_id => id, :action => 'session')
    token.value
  end

423 424 425 426 427 428 429 430 431 432 433 434 435
  def delete_session_token(value)
    Token.where(:user_id => id, :action => 'session', :value => value).delete_all
  end

  # Generates a new autologin token and returns its value
  def generate_autologin_token
    token = Token.create!(:user_id => id, :action => 'autologin')
    token.value
  end

  def delete_autologin_token(value)
    Token.where(:user_id => id, :action => 'autologin', :value => value).delete_all
  end
436

437 438 439 440 441 442 443 444 445 446 447 448 449 450
  # Returns true if token is a valid session token for the user whose id is user_id
  def self.verify_session_token(user_id, token)
    return false if user_id.blank? || token.blank?

    scope = Token.where(:user_id => user_id, :value => token.to_s, :action => 'session')
    if Setting.session_lifetime?
      scope = scope.where("created_on > ?", Setting.session_lifetime.to_i.minutes.ago)
    end
    if Setting.session_timeout?
      scope = scope.where("updated_on > ?", Setting.session_timeout.to_i.minutes.ago)
    end
    scope.update_all(:updated_on => Time.now) == 1
  end

451 452 453 454
  # Return an array of project ids for which the user has explicitly turned mail notifications on
  def notified_projects_ids
    @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
  end
455

456
  def notified_project_ids=(ids)
457
    @notified_projects_ids_changed = true
458
    @notified_projects_ids = ids.map(&:to_i).uniq.select {|n| n > 0}
459 460 461 462 463 464 465 466 467
  end

  # Updates per project notifications (after_save callback)
  def update_notified_project_ids
    if @notified_projects_ids_changed
      ids = (mail_notification == 'selected' ? Array.wrap(notified_projects_ids).reject(&:blank?) : [])
      members.update_all(:mail_notification => false)
      members.where(:project_id => ids).update_all(:mail_notification => true) if ids.any?
    end
468
  end
469
  private :update_notified_project_ids
470

edavis10's avatar
edavis10 committed
471
  def valid_notification_options
472 473 474 475 476
    self.class.valid_notification_options(self)
  end

  # Only users that belong to more than 1 project can select projects for which they are notified
  def self.valid_notification_options(user=nil)
edavis10's avatar
edavis10 committed
477 478
    # Note that @user.membership.size would fail since AR ignores
    # :include association option when doing a count
479
    if user.nil? || user.memberships.length < 1
480
      MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
edavis10's avatar
edavis10 committed
481 482 483 484 485
    else
      MAIL_NOTIFICATION_OPTIONS
    end
  end

486 487
  # Find a user account by matching the exact login and then a case-insensitive
  # version.  Exact matches will be given priority.
488
  def self.find_by_login(login)
489
    login = Redmine::CodesetUtil.replace_invalid_utf8(login.to_s)
jplang's avatar
jplang committed
490 491
    if login.present?
      # First look for an exact match
492
      user = where(:login => login).detect {|u| u.login == login}
jplang's avatar
jplang committed
493 494
      unless user
        # Fail over to case-insensitive if none was found
495
        user = find_by("LOWER(login) = ?", login.downcase)
jplang's avatar
jplang committed
496 497
      end
      user
jplang's avatar
jplang committed
498
    end
499 500
  end

501
  def self.find_by_rss_key(key)
502
    Token.find_active_user('feeds', key)
503
  end
504

505
  def self.find_by_api_key(key)
506
    Token.find_active_user('api', key)
507
  end
508

509 510
  # Makes find_by_mail case-insensitive
  def self.find_by_mail(mail)
511
    having_mail(mail).first
512
  end
513

514 515 516 517 518
  # Returns true if the default admin account can no longer be used
  def self.default_admin_account_changed?
    !User.active.find_by_login("admin").try(:check_password?, "admin")
  end

jplang's avatar
jplang committed
519 520 521
  def to_s
    name
  end
522

523
  LABEL_BY_STATUS = {
524 525 526 527 528 529 530
    STATUS_ANONYMOUS  => 'anon',
    STATUS_ACTIVE     => 'active',
    STATUS_REGISTERED => 'registered',
    STATUS_LOCKED     => 'locked'
  }

  def css_classes
531
    "user #{LABEL_BY_STATUS[status]}"
532 533
  end

534 535 536 537 538
  # Returns the current day according to user's time zone
  def today
    if time_zone.nil?
      Date.today
    else
jplang's avatar
jplang committed
539
      time_zone.today
540 541
    end
  end
542

543 544 545 546 547 548 549 550 551
  # Returns the day of +time+ according to user's time zone
  def time_to_date(time)
    if time_zone.nil?
      time.to_date
    else
      time.in_time_zone(time_zone).to_date
    end
  end

jplang's avatar
jplang committed
552 553 554
  def logged?
    true
  end
555

556 557 558
  def anonymous?
    !logged?
  end
559

560 561 562 563 564 565 566 567 568 569 570
  # Returns user's membership for the given project
  # or nil if the user is not a member of project
  def membership(project)
    project_id = project.is_a?(Project) ? project.id : project

    @membership_by_project_id ||= Hash.new {|h, project_id|
      h[project_id] = memberships.where(:project_id => project_id).first
    }
    @membership_by_project_id[project_id]
  end

571
  def roles
jplang's avatar
jplang committed
572
    @roles ||= Role.joins(members: :project).where(["#{Project.table_name}.status <> ?", Project::STATUS_ARCHIVED]).where(Member.arel_table[:user_id].eq(id)).distinct
573 574
  end

jplang's avatar
jplang committed
575 576
  # Returns the user's bult-in role
  def builtin_role
jplang's avatar
jplang committed
577
    @builtin_role ||= Role.non_member
jplang's avatar
jplang committed
578 579
  end

580 581
  # Return user's roles for project
  def roles_for_project(project)
jplang's avatar
jplang committed
582
    # No role on archived projects
583
    return [] if project.nil? || project.archived?
jplang's avatar
jplang committed
584
    if membership = membership(project)
585
      membership.roles.to_a
586 587
    elsif project.is_public?
      project.override_roles(builtin_role)
jplang's avatar
jplang committed
588
    else
589
      []
jplang's avatar
jplang committed
590 591
    end
  end
592

593
  # Returns a hash of user's projects grouped by roles
594
  # TODO: No longer used, should be deprecated
595 596
  def projects_by_role
    return @projects_by_role if @projects_by_role
597

598 599 600 601 602 603 604 605 606 607 608
    result = Hash.new([])
    project_ids_by_role.each do |role, ids|
      result[role] = Project.where(:id => ids).to_a
    end
    @projects_by_role = result
  end

  # Returns a hash of project ids grouped by roles.
  # Includes the projects that the user is a member of and the projects
  # that grant custom permissions to the builtin groups.
  def project_ids_by_role
609 610 611 612
    # Clear project condition for when called from chained scopes
    # eg. project.children.visible(user)
    Project.unscoped do
      return @project_ids_by_role if @project_ids_by_role
613

614 615
      group_class = anonymous? ? GroupAnonymous : GroupNonMember
      group_id = group_class.pluck(:id).first
616

617 618 619 620
      members = Member.joins(:project, :member_roles).
        where("#{Project.table_name}.status <> 9").
        where("#{Member.table_name}.user_id = ? OR (#{Project.table_name}.is_public = ? AND #{Member.table_name}.user_id = ?)", self.id, true, group_id).
        pluck(:user_id, :role_id, :project_id)
621

622 623 624 625
      hash = {}
      members.each do |user_id, role_id, project_id|
        # Ignore the roles of the builtin group if the user is a member of the project
        next if user_id != id && project_ids.include?(project_id)
626

627 628 629
        hash[role_id] ||= []
        hash[role_id] << project_id
      end
630

631 632 633 634 635 636 637 638
      result = Hash.new([])
      if hash.present?
        roles = Role.where(:id => hash.keys).to_a
        hash.each do |role_id, proj_ids|
          role = roles.detect {|r| r.id == role_id}
          if role
            result[role] = proj_ids.uniq
          end
639 640
        end
      end
641
      @project_ids_by_role = result
642
    end
643
  end
644

645 646 647 648 649
  # Returns the ids of visible projects
  def visible_project_ids
    @visible_project_ids ||= Project.visible(self).pluck(:id)
  end

650 651 652
  # Returns the roles that the user is allowed to manage for the given project
  def managed_roles(project)
    if admin?
653
      @managed_roles ||= Role.givable.to_a
654 655 656 657 658
    else
      membership(project).try(:managed_roles) || []
    end
  end

659 660 661 662 663 664 665 666 667 668
  # Returns true if user is arg or belongs to arg
  def is_or_belongs_to?(arg)
    if arg.is_a?(User)
      self == arg
    elsif arg.is_a?(Group)
      arg.users.include?(self)
    else
      false
    end
  end
669

670 671
  # Return true if the user is allowed to do the specified action on a specific context
  # Action can be:
jplang's avatar
jplang committed
672 673
  # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
  # * a permission Symbol (eg. :edit_project)
674 675
  # Context can be:
  # * a project : returns true if user is allowed to do the specified action on this project
676
  # * an array of projects : returns true if user is allowed on every project
677
  # * nil with options[:global] set : check if user has at least one role allowed for this action,
678
  #   or falls back to Non Member / Anonymous permissions depending if the user is logged
679
  def allowed_to?(action, context, options={}, &block)
680 681
    if context && context.is_a?(Project)
      return false unless context.allows_to?(action)
682 683
      # Admin users are authorized for anything else
      return true if admin?
684

685
      roles = roles_for_project(context)
686
      return false unless roles
687
      roles.any? {|role|
688 689 690 691
        (context.is_public? || role.member?) &&
        role.allowed_to?(action) &&
        (block_given? ? yield(role, self) : true)
      }
692
    elsif context && context.is_a?(Array)
693 694 695 696 697
      if context.empty?
        false
      else
        # Authorize if user is authorized on every element of the array
        context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
698
      end
jplang's avatar
jplang committed
699 700
    elsif context
      raise ArgumentError.new("#allowed_to? context argument must be a Project, an Array of projects or nil")
701
    elsif options[:global]
702 703
      # Admin users are always authorized
      return true if admin?
704

705
      # authorize if user has at least one role that has this permission
706 707
      roles = self.roles.to_a | [builtin_role]
      roles.any? {|role|
708 709 710
        role.allowed_to?(action) &&
        (block_given? ? yield(role, self) : true)
      }
711 712 713
    else
      false
    end
jplang's avatar
jplang committed
714
  end
715 716 717

  # Is the user allowed to do the specified action on any project?
  # See allowed_to? for the actions and valid options.
718 719 720 721 722
  #
  # NB: this method is not used anywhere in the core codebase as of
  # 2.5.2, but it's used by many plugins so if we ever want to remove
  # it it has to be carefully deprecated for a version or two.
  def allowed_to_globally?(action, options={}, &block)
723
    allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
724
  end
725

726 727 728 729 730 731
  def allowed_to_view_all_time_entries?(context)
    allowed_to?(:view_time_entries, context) do |role, user|
      role.time_entries_visibility == 'all'
    end
  end

732
  # Returns true if the user is allowed to delete the user's own account
733 734
  def own_account_deletable?
    Setting.unsubscribe? &&
jplang's avatar
jplang committed
735
      (!admin? || User.active.admin.where("id <> ?", id).exists?)
736 737
  end

738
  safe_attributes 'firstname',
739 740 741
    'lastname',
    'mail',
    'mail_notification',
742
    'notified_project_ids',
743 744 745 746
    'language',
    'custom_field_values',
    'custom_fields',
    'identity_url'
747

748 749 750
  safe_attributes 'login',
    :if => lambda {|user, current_user| user.new_record?}

751 752
  safe_attributes 'status',
    'auth_source_id',
753
    'generate_password',
754
    'must_change_passwd',
755 756
    'login',
    'admin',
757
    :if => lambda {|user, current_user| current_user.admin?}
758

759 760
  safe_attributes 'group_ids',
    :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
761

762 763 764 765 766
  # Utility method to help check if a user should be notified about an
  # event.
  #
  # TODO: only supports Issue events currently
  def notify_about?(object)
767
    if mail_notification == 'all'
768
      true
769
    elsif mail_notification.blank? || mail_notification == 'none'
770
      false
771 772 773 774 775 776
    else
      case object
      when Issue
        case mail_notification
        when 'selected', 'only_my_events'
          # user receives notifications for created/assigned issues on unselected projects
jplang's avatar
jplang committed
777
          object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.previous_assignee)
778
        when 'only_assigned'
jplang's avatar
jplang committed
779
          is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.previous_assignee)
780 781 782 783 784
        when 'only_owner'
          object.author == self
        end
      when News
        # always send to project members except when mail_notification is set to 'none'
785 786 787 788
        true
      end
    end
  end
789

jplang's avatar
jplang committed
790
  def self.current=(user)
791
    RequestStore.store[:current_user] = user
jplang's avatar
jplang committed
792
  end
793

jplang's avatar
jplang committed
794
  def self.current
795
    RequestStore.store[:current_user] ||= User.anonymous
jplang's avatar
jplang committed
796
  end
797

798 799
  # Returns the anonymous user.  If the anonymous user does not exist, it is created.  There can be only
  # one anonymous user per database.
jplang's avatar
jplang committed
800
  def self.anonymous
801
    anonymous_user = AnonymousUser.unscoped.find_by(:lastname => 'Anonymous')
802
    if anonymous_user.nil?
803
      anonymous_user = AnonymousUser.unscoped.create(:lastname => 'Anonymous', :firstname => '', :login => '', :status => 0)
804 805
      raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
    end
806
    anonymous_user
jplang's avatar
jplang committed
807
  end
808 809 810 811 812 813

  # Salts all existing unsalted passwords
  # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
  # This method is used in the SaltPasswords migration and is to be kept as is
  def self.salt_unsalted_passwords!
    transaction do
jplang's avatar
jplang committed
814
      User.where("salt IS NULL OR salt = ''").find_each do |user|
815 816 817
        next if user.hashed_password.blank?
        salt = User.generate_salt
        hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
jplang's avatar
jplang committed
818
        User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
819 820 821
      end
    end
  end
822

823
  protected
824

825
  def validate_password_length
826
    return if password.blank? && generate_password?
827 828 829 830 831
    # Password length validation based on setting
    if !password.nil? && password.size < Setting.password_min_length.to_i
      errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
    end
  end
832

833 834 835 836
  def instantiate_email_address
    email_address || build_email_address
  end

837
  private
838

839 840 841 842 843 844 845
  def generate_password_if_needed
    if generate_password? && auth_source.nil?
      length = [Setting.password_min_length.to_i + 2, 10].max
      random_password(length)
    end
  end

846
  # Delete all outstanding password reset tokens on password change.
847 848 849 850
  # Delete the autologin tokens on password change to prohibit session leakage.
  # This helps to keep the account secure in case the associated email account
  # was compromised.
  def destroy_tokens
jplang's avatar
jplang committed
851
    if saved_change_to_hashed_password? || (saved_change_to_status? && !active?)
852
      tokens = ['recovery', 'autologin', 'session']
jplang's avatar
jplang committed
853 854
      Token.where(:user_id => id, :action => tokens).delete_all
    end
855 856
  end

857 858 859 860
  # Removes references that are not handled by associations
  # Things that are not deleted are reassociated with the anonymous user
  def remove_references_before_destroy
    return if self.id.nil?
861

862
    substitute = User.anonymous
863
    Attachment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
864 865 866
    Comment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
    Issue.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
    Issue.where(['assigned_to_id = ?', id]).update_all('assigned_to_id = NULL')
867
    Journal.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
868 869 870 871 872
    JournalDetail.
      where(["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]).
      update_all(['old_value = ?', substitute.id.to_s])
    JournalDetail.
      where(["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]).
873
      update_all(['value = ?', substitute.id.to_s])
874 875
    Message.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
    News.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
876
    # Remove private queries and keep public ones
877
    ::Query.where('user_id = ? AND visibility = ?', id, ::Query::VISIBILITY_PRIVATE).delete_all
878
    ::Query.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
879
    TimeEntry.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
880 881
    Token.where('user_id = ?', id).delete_all
    Watcher.where('user_id = ?', id).delete_all
882 883
    WikiContent.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
    WikiContent::Version.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
884
  end
885

886 887 888
  # Return password digest
  def self.hash_password(clear_password)
    Digest::SHA1.hexdigest(clear_password || "")
jplang's avatar
jplang committed
889
  end
890

891 892
  # Returns a 128bits random salt as a hex string (32 chars long)
  def self.generate_salt
893
    Redmine::Utils.random_hex(16)
894
  end
895

896 897 898 899 900 901 902 903
  # Send a security notification to all admins if the user has gained/lost admin privileges
  def deliver_security_notification
    options = {
      field: :field_admin,
      value: login,
      title: :label_user_plural,
      url: {controller: 'users', action: 'index'}
    }
904

905
    deliver = false
jplang's avatar
jplang committed
906 907 908
    if (admin? && saved_change_to_id? && active?) ||    # newly created admin
       (admin? && saved_change_to_admin? && active?) || # regular user became admin
       (admin? && saved_change_to_status? && active?)   # locked admin became active again
909 910 911 912 913

       deliver = true
       options[:message] = :mail_body_security_notification_add

    elsif (admin? && destroyed? && active?) ||      # active admin user was deleted
jplang's avatar
jplang committed
914 915
          (!admin? && saved_change_to_admin? && active?) || # admin is no longer admin
          (admin? && saved_change_to_status? && !active?)   # admin was locked
916 917 918 919 920

          deliver = true
          options[:message] = :mail_body_security_notification_remove
    end

921 922 923 924
    if deliver
      users = User.active.where(admin: true).to_a
      Mailer.security_notification(users, options).deliver
    end
925
  end
jplang's avatar
jplang committed
926
end
jplang's avatar
jplang committed
927 928

class AnonymousUser < User
jplang's avatar
jplang committed
929
  validate :validate_anonymous_uniqueness, :on => :create
930

931 932
  self.valid_statuses = [STATUS_ANONYMOUS]

jplang's avatar
jplang committed
933
  def validate_anonymous_uniqueness
934
    # There should be only one AnonymousUser in the database
jplang's avatar
jplang committed
935
    errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
936
  end
937

938 939 940
  def available_custom_fields
    []
  end
941

942 943 944
  # Overrides a few properties
  def logged?; false end
  def admin; false end
945
  def name(*args); I18n.t(:label_user_anonymous) end
946
  def mail=(*args); nil end
947 948 949
  def mail; nil end
  def time_zone; nil end
  def rss_key; nil end
950

951 952 953 954
  def pref
    UserPreference.new(:user => self)
  end

jplang's avatar
jplang committed
955 956 957 958 959 960 961 962 963 964
  # Returns the user's bult-in role
  def builtin_role
    @builtin_role ||= Role.anonymous
  end

  def membership(*args)
    nil
  end

  def member_of?(*args)
965 966 967
    false
  end

968 969 970 971
  # Anonymous user can not be destroyed
  def destroy
    false
  end
972 973 974 975 976

  protected

  def instantiate_email_address
  end