annotate app/models/user.rb @ 8:0c83d98252d9 yuya

* Add custom repo prefix and proper auth realm, remove auth cache (seems like an unwise feature), pass DB handle around, various other bits of tidying
author Chris Cannam
date Thu, 12 Aug 2010 15:31:37 +0100
parents cca12e1c1fd4
children 1d32c0a0efbf
rev   line source
Chris@0 1 # Redmine - project management software
Chris@0 2 # Copyright (C) 2006-2009 Jean-Philippe Lang
Chris@0 3 #
Chris@0 4 # This program is free software; you can redistribute it and/or
Chris@0 5 # modify it under the terms of the GNU General Public License
Chris@0 6 # as published by the Free Software Foundation; either version 2
Chris@0 7 # of the License, or (at your option) any later version.
Chris@0 8 #
Chris@0 9 # This program is distributed in the hope that it will be useful,
Chris@0 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
Chris@0 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Chris@0 12 # GNU General Public License for more details.
Chris@0 13 #
Chris@0 14 # You should have received a copy of the GNU General Public License
Chris@0 15 # along with this program; if not, write to the Free Software
Chris@0 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
Chris@0 17
Chris@0 18 require "digest/sha1"
Chris@0 19
Chris@0 20 class User < Principal
Chris@0 21
Chris@0 22 # Account statuses
Chris@0 23 STATUS_ANONYMOUS = 0
Chris@0 24 STATUS_ACTIVE = 1
Chris@0 25 STATUS_REGISTERED = 2
Chris@0 26 STATUS_LOCKED = 3
Chris@0 27
Chris@0 28 USER_FORMATS = {
Chris@0 29 :firstname_lastname => '#{firstname} #{lastname}',
Chris@0 30 :firstname => '#{firstname}',
Chris@0 31 :lastname_firstname => '#{lastname} #{firstname}',
Chris@0 32 :lastname_coma_firstname => '#{lastname}, #{firstname}',
Chris@0 33 :username => '#{login}'
Chris@0 34 }
Chris@0 35
Chris@0 36 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
Chris@0 37 :after_remove => Proc.new {|user, group| group.user_removed(user)}
Chris@0 38 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
Chris@0 39 has_many :changesets, :dependent => :nullify
Chris@0 40 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
Chris@0 41 has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
Chris@0 42 has_one :api_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='api'"
Chris@0 43 belongs_to :auth_source
Chris@0 44
Chris@0 45 # Active non-anonymous users scope
Chris@0 46 named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
Chris@0 47
Chris@0 48 acts_as_customizable
Chris@0 49
Chris@0 50 attr_accessor :password, :password_confirmation
Chris@0 51 attr_accessor :last_before_login_on
Chris@0 52 # Prevents unauthorized assignments
Chris@0 53 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password, :group_ids
Chris@0 54
Chris@0 55 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
Chris@0 56 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }, :case_sensitive => false
Chris@0 57 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
Chris@0 58 # Login must contain lettres, numbers, underscores only
Chris@0 59 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
Chris@0 60 validates_length_of :login, :maximum => 30
Chris@0 61 validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-\.]*$/i
Chris@0 62 validates_length_of :firstname, :lastname, :maximum => 30
Chris@0 63 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
Chris@0 64 validates_length_of :mail, :maximum => 60, :allow_nil => true
Chris@0 65 validates_confirmation_of :password, :allow_nil => true
Chris@0 66
Chris@0 67 def before_create
Chris@0 68 self.mail_notification = false
Chris@0 69 true
Chris@0 70 end
Chris@0 71
Chris@0 72 def before_save
Chris@0 73 # update hashed_password if password was set
Chris@0 74 self.hashed_password = User.hash_password(self.password) if self.password && self.auth_source_id.blank?
Chris@0 75 end
Chris@0 76
Chris@0 77 def reload(*args)
Chris@0 78 @name = nil
Chris@0 79 super
Chris@0 80 end
Chris@0 81
Chris@1 82 def mail=(arg)
Chris@1 83 write_attribute(:mail, arg.to_s.strip)
Chris@1 84 end
Chris@1 85
Chris@0 86 def identity_url=(url)
Chris@0 87 if url.blank?
Chris@0 88 write_attribute(:identity_url, '')
Chris@0 89 else
Chris@0 90 begin
Chris@0 91 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
Chris@0 92 rescue OpenIdAuthentication::InvalidOpenId
Chris@0 93 # Invlaid url, don't save
Chris@0 94 end
Chris@0 95 end
Chris@0 96 self.read_attribute(:identity_url)
Chris@0 97 end
Chris@0 98
Chris@0 99 # Returns the user that matches provided login and password, or nil
Chris@0 100 def self.try_to_login(login, password)
Chris@0 101 # Make sure no one can sign in with an empty password
Chris@0 102 return nil if password.to_s.empty?
Chris@0 103 user = find_by_login(login)
Chris@0 104 if user
Chris@0 105 # user is already in local database
Chris@0 106 return nil if !user.active?
Chris@0 107 if user.auth_source
Chris@0 108 # user has an external authentication method
Chris@0 109 return nil unless user.auth_source.authenticate(login, password)
Chris@0 110 else
Chris@0 111 # authentication with local password
Chris@0 112 return nil unless User.hash_password(password) == user.hashed_password
Chris@0 113 end
Chris@0 114 else
Chris@0 115 # user is not yet registered, try to authenticate with available sources
Chris@0 116 attrs = AuthSource.authenticate(login, password)
Chris@0 117 if attrs
Chris@0 118 user = new(attrs)
Chris@0 119 user.login = login
Chris@0 120 user.language = Setting.default_language
Chris@0 121 if user.save
Chris@0 122 user.reload
Chris@0 123 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
Chris@0 124 end
Chris@0 125 end
Chris@0 126 end
Chris@0 127 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
Chris@0 128 user
Chris@0 129 rescue => text
Chris@0 130 raise text
Chris@0 131 end
Chris@0 132
Chris@0 133 # Returns the user who matches the given autologin +key+ or nil
Chris@0 134 def self.try_to_autologin(key)
Chris@0 135 tokens = Token.find_all_by_action_and_value('autologin', key)
Chris@0 136 # Make sure there's only 1 token that matches the key
Chris@0 137 if tokens.size == 1
Chris@0 138 token = tokens.first
Chris@0 139 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
Chris@0 140 token.user.update_attribute(:last_login_on, Time.now)
Chris@0 141 token.user
Chris@0 142 end
Chris@0 143 end
Chris@0 144 end
Chris@0 145
Chris@0 146 # Return user's full name for display
Chris@0 147 def name(formatter = nil)
Chris@0 148 if formatter
Chris@0 149 eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
Chris@0 150 else
Chris@0 151 @name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
Chris@0 152 end
Chris@0 153 end
Chris@0 154
Chris@0 155 def active?
Chris@0 156 self.status == STATUS_ACTIVE
Chris@0 157 end
Chris@0 158
Chris@0 159 def registered?
Chris@0 160 self.status == STATUS_REGISTERED
Chris@0 161 end
Chris@0 162
Chris@0 163 def locked?
Chris@0 164 self.status == STATUS_LOCKED
Chris@0 165 end
Chris@0 166
Chris@0 167 def check_password?(clear_password)
Chris@0 168 if auth_source_id.present?
Chris@0 169 auth_source.authenticate(self.login, clear_password)
Chris@0 170 else
Chris@0 171 User.hash_password(clear_password) == self.hashed_password
Chris@0 172 end
Chris@0 173 end
Chris@0 174
Chris@0 175 # Does the backend storage allow this user to change their password?
Chris@0 176 def change_password_allowed?
Chris@0 177 return true if auth_source_id.blank?
Chris@0 178 return auth_source.allow_password_changes?
Chris@0 179 end
Chris@0 180
Chris@0 181 # Generate and set a random password. Useful for automated user creation
Chris@0 182 # Based on Token#generate_token_value
Chris@0 183 #
Chris@0 184 def random_password
Chris@0 185 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
Chris@0 186 password = ''
Chris@0 187 40.times { |i| password << chars[rand(chars.size-1)] }
Chris@0 188 self.password = password
Chris@0 189 self.password_confirmation = password
Chris@0 190 self
Chris@0 191 end
Chris@0 192
Chris@0 193 def pref
Chris@0 194 self.preference ||= UserPreference.new(:user => self)
Chris@0 195 end
Chris@0 196
Chris@0 197 def time_zone
Chris@0 198 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
Chris@0 199 end
Chris@0 200
Chris@0 201 def wants_comments_in_reverse_order?
Chris@0 202 self.pref[:comments_sorting] == 'desc'
Chris@0 203 end
Chris@0 204
Chris@0 205 # Return user's RSS key (a 40 chars long string), used to access feeds
Chris@0 206 def rss_key
Chris@0 207 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
Chris@0 208 token.value
Chris@0 209 end
Chris@0 210
Chris@0 211 # Return user's API key (a 40 chars long string), used to access the API
Chris@0 212 def api_key
Chris@0 213 token = self.api_token || self.create_api_token(:action => 'api')
Chris@0 214 token.value
Chris@0 215 end
Chris@0 216
Chris@0 217 # Return an array of project ids for which the user has explicitly turned mail notifications on
Chris@0 218 def notified_projects_ids
Chris@0 219 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
Chris@0 220 end
Chris@0 221
Chris@0 222 def notified_project_ids=(ids)
Chris@0 223 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
Chris@0 224 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
Chris@0 225 @notified_projects_ids = nil
Chris@0 226 notified_projects_ids
Chris@0 227 end
Chris@0 228
Chris@0 229 # Find a user account by matching the exact login and then a case-insensitive
Chris@0 230 # version. Exact matches will be given priority.
Chris@0 231 def self.find_by_login(login)
Chris@0 232 # force string comparison to be case sensitive on MySQL
Chris@0 233 type_cast = (ActiveRecord::Base.connection.adapter_name == 'MySQL') ? 'BINARY' : ''
Chris@0 234
Chris@0 235 # First look for an exact match
Chris@0 236 user = first(:conditions => ["#{type_cast} login = ?", login])
Chris@0 237 # Fail over to case-insensitive if none was found
Chris@0 238 user ||= first(:conditions => ["#{type_cast} LOWER(login) = ?", login.to_s.downcase])
Chris@0 239 end
Chris@0 240
Chris@0 241 def self.find_by_rss_key(key)
Chris@0 242 token = Token.find_by_value(key)
Chris@0 243 token && token.user.active? ? token.user : nil
Chris@0 244 end
Chris@0 245
Chris@0 246 def self.find_by_api_key(key)
Chris@0 247 token = Token.find_by_action_and_value('api', key)
Chris@0 248 token && token.user.active? ? token.user : nil
Chris@0 249 end
Chris@0 250
Chris@0 251 # Makes find_by_mail case-insensitive
Chris@0 252 def self.find_by_mail(mail)
Chris@0 253 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
Chris@0 254 end
Chris@0 255
Chris@0 256 def to_s
Chris@0 257 name
Chris@0 258 end
Chris@0 259
Chris@0 260 # Returns the current day according to user's time zone
Chris@0 261 def today
Chris@0 262 if time_zone.nil?
Chris@0 263 Date.today
Chris@0 264 else
Chris@0 265 Time.now.in_time_zone(time_zone).to_date
Chris@0 266 end
Chris@0 267 end
Chris@0 268
Chris@0 269 def logged?
Chris@0 270 true
Chris@0 271 end
Chris@0 272
Chris@0 273 def anonymous?
Chris@0 274 !logged?
Chris@0 275 end
Chris@0 276
Chris@0 277 # Return user's roles for project
Chris@0 278 def roles_for_project(project)
Chris@0 279 roles = []
Chris@0 280 # No role on archived projects
Chris@0 281 return roles unless project && project.active?
Chris@0 282 if logged?
Chris@0 283 # Find project membership
Chris@0 284 membership = memberships.detect {|m| m.project_id == project.id}
Chris@0 285 if membership
Chris@0 286 roles = membership.roles
Chris@0 287 else
Chris@0 288 @role_non_member ||= Role.non_member
Chris@0 289 roles << @role_non_member
Chris@0 290 end
Chris@0 291 else
Chris@0 292 @role_anonymous ||= Role.anonymous
Chris@0 293 roles << @role_anonymous
Chris@0 294 end
Chris@0 295 roles
Chris@0 296 end
Chris@0 297
Chris@0 298 # Return true if the user is a member of project
Chris@0 299 def member_of?(project)
Chris@0 300 !roles_for_project(project).detect {|role| role.member?}.nil?
Chris@0 301 end
Chris@0 302
Chris@0 303 # Return true if the user is allowed to do the specified action on project
Chris@0 304 # action can be:
Chris@0 305 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
Chris@0 306 # * a permission Symbol (eg. :edit_project)
Chris@0 307 def allowed_to?(action, project, options={})
Chris@0 308 if project
Chris@0 309 # No action allowed on archived projects
Chris@0 310 return false unless project.active?
Chris@0 311 # No action allowed on disabled modules
Chris@0 312 return false unless project.allows_to?(action)
Chris@0 313 # Admin users are authorized for anything else
Chris@0 314 return true if admin?
Chris@0 315
Chris@0 316 roles = roles_for_project(project)
Chris@0 317 return false unless roles
Chris@0 318 roles.detect {|role| (project.is_public? || role.member?) && role.allowed_to?(action)}
Chris@0 319
Chris@0 320 elsif options[:global]
Chris@0 321 # Admin users are always authorized
Chris@0 322 return true if admin?
Chris@0 323
Chris@0 324 # authorize if user has at least one role that has this permission
Chris@0 325 roles = memberships.collect {|m| m.roles}.flatten.uniq
Chris@0 326 roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
Chris@0 327 else
Chris@0 328 false
Chris@0 329 end
Chris@0 330 end
Chris@0 331
Chris@0 332 def self.current=(user)
Chris@0 333 @current_user = user
Chris@0 334 end
Chris@0 335
Chris@0 336 def self.current
Chris@0 337 @current_user ||= User.anonymous
Chris@0 338 end
Chris@0 339
Chris@0 340 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
Chris@0 341 # one anonymous user per database.
Chris@0 342 def self.anonymous
Chris@0 343 anonymous_user = AnonymousUser.find(:first)
Chris@0 344 if anonymous_user.nil?
Chris@0 345 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
Chris@0 346 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
Chris@0 347 end
Chris@0 348 anonymous_user
Chris@0 349 end
Chris@0 350
Chris@0 351 protected
Chris@0 352
Chris@0 353 def validate
Chris@0 354 # Password length validation based on setting
Chris@0 355 if !password.nil? && password.size < Setting.password_min_length.to_i
Chris@0 356 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
Chris@0 357 end
Chris@0 358 end
Chris@0 359
Chris@0 360 private
Chris@0 361
Chris@0 362 # Return password digest
Chris@0 363 def self.hash_password(clear_password)
Chris@0 364 Digest::SHA1.hexdigest(clear_password || "")
Chris@0 365 end
Chris@0 366 end
Chris@0 367
Chris@0 368 class AnonymousUser < User
Chris@0 369
Chris@0 370 def validate_on_create
Chris@0 371 # There should be only one AnonymousUser in the database
Chris@0 372 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
Chris@0 373 end
Chris@0 374
Chris@0 375 def available_custom_fields
Chris@0 376 []
Chris@0 377 end
Chris@0 378
Chris@0 379 # Overrides a few properties
Chris@0 380 def logged?; false end
Chris@0 381 def admin; false end
Chris@0 382 def name(*args); I18n.t(:label_user_anonymous) end
Chris@0 383 def mail; nil end
Chris@0 384 def time_zone; nil end
Chris@0 385 def rss_key; nil end
Chris@0 386 end