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