|
1 |
# Redmine - project management software
|
|
2 |
# Copyright (C) 2006-2012 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 |
# Different ways of displaying/sorting users
|
|
30 |
USER_FORMATS = {
|
|
31 |
:firstname_lastname => {
|
|
32 |
:string => '#{firstname} #{lastname}',
|
|
33 |
:order => %w(firstname lastname id),
|
|
34 |
:setting_order => 1
|
|
35 |
},
|
|
36 |
:firstname_lastinitial => {
|
|
37 |
:string => '#{firstname} #{lastname.to_s.chars.first}.',
|
|
38 |
:order => %w(firstname lastname id),
|
|
39 |
:setting_order => 2
|
|
40 |
},
|
|
41 |
:firstname => {
|
|
42 |
:string => '#{firstname}',
|
|
43 |
:order => %w(firstname id),
|
|
44 |
:setting_order => 3
|
|
45 |
},
|
|
46 |
:lastname_firstname => {
|
|
47 |
:string => '#{lastname} #{firstname}',
|
|
48 |
:order => %w(lastname firstname id),
|
|
49 |
:setting_order => 4
|
|
50 |
},
|
|
51 |
:lastname_coma_firstname => {
|
|
52 |
:string => '#{lastname}, #{firstname}',
|
|
53 |
:order => %w(lastname firstname id),
|
|
54 |
:setting_order => 5
|
|
55 |
},
|
|
56 |
:lastname => {
|
|
57 |
:string => '#{lastname}',
|
|
58 |
:order => %w(lastname id),
|
|
59 |
:setting_order => 6
|
|
60 |
},
|
|
61 |
:username => {
|
|
62 |
:string => '#{login}',
|
|
63 |
:order => %w(login id),
|
|
64 |
:setting_order => 7
|
|
65 |
},
|
|
66 |
}
|
|
67 |
|
|
68 |
MAIL_NOTIFICATION_OPTIONS = [
|
|
69 |
['all', :label_user_mail_option_all],
|
|
70 |
['selected', :label_user_mail_option_selected],
|
|
71 |
['only_my_events', :label_user_mail_option_only_my_events],
|
|
72 |
['only_assigned', :label_user_mail_option_only_assigned],
|
|
73 |
['only_owner', :label_user_mail_option_only_owner],
|
|
74 |
['none', :label_user_mail_option_none]
|
|
75 |
]
|
|
76 |
|
|
77 |
has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
|
|
78 |
:after_remove => Proc.new {|user, group| group.user_removed(user)}
|
|
79 |
has_many :changesets, :dependent => :nullify
|
|
80 |
has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
|
|
81 |
has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
|
|
82 |
has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
|
|
83 |
belongs_to :auth_source
|
|
84 |
|
|
85 |
scope :logged, :conditions => "#{User.table_name}.status <> #{STATUS_ANONYMOUS}"
|
|
86 |
scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
|
|
87 |
|
|
88 |
acts_as_customizable
|
|
89 |
|
|
90 |
attr_accessor :password, :password_confirmation
|
|
91 |
attr_accessor :last_before_login_on
|
|
92 |
# Prevents unauthorized assignments
|
|
93 |
attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
|
|
94 |
|
|
95 |
LOGIN_LENGTH_LIMIT = 60
|
|
96 |
MAIL_LENGTH_LIMIT = 60
|
|
97 |
|
|
98 |
validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
|
|
99 |
validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
|
|
100 |
validates_uniqueness_of :mail, :if => Proc.new { |user| user.mail_changed? && user.mail.present? }, :case_sensitive => false
|
|
101 |
# Login must contain lettres, numbers, underscores only
|
|
102 |
validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
|
|
103 |
validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
|
|
104 |
validates_length_of :firstname, :lastname, :maximum => 30
|
|
105 |
validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_blank => true
|
|
106 |
validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true
|
|
107 |
validates_confirmation_of :password, :allow_nil => true
|
|
108 |
validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
|
|
109 |
validate :validate_password_length
|
|
110 |
|
|
111 |
before_create :set_mail_notification
|
|
112 |
before_save :update_hashed_password
|
|
113 |
before_destroy :remove_references_before_destroy
|
|
114 |
|
|
115 |
scope :in_group, lambda {|group|
|
|
116 |
group_id = group.is_a?(Group) ? group.id : group.to_i
|
|
117 |
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)
|
|
118 |
}
|
|
119 |
scope :not_in_group, lambda {|group|
|
|
120 |
group_id = group.is_a?(Group) ? group.id : group.to_i
|
|
121 |
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)
|
|
122 |
}
|
|
123 |
|
|
124 |
def set_mail_notification
|
|
125 |
self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
|
|
126 |
true
|
|
127 |
end
|
|
128 |
|
|
129 |
def update_hashed_password
|
|
130 |
# update hashed_password if password was set
|
|
131 |
if self.password && self.auth_source_id.blank?
|
|
132 |
salt_password(password)
|
|
133 |
end
|
|
134 |
end
|
|
135 |
|
|
136 |
def reload(*args)
|
|
137 |
@name = nil
|
|
138 |
@projects_by_role = nil
|
|
139 |
super
|
|
140 |
end
|
|
141 |
|
|
142 |
def mail=(arg)
|
|
143 |
write_attribute(:mail, arg.to_s.strip)
|
|
144 |
end
|
|
145 |
|
|
146 |
def identity_url=(url)
|
|
147 |
if url.blank?
|
|
148 |
write_attribute(:identity_url, '')
|
|
149 |
else
|
|
150 |
begin
|
|
151 |
write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
|
|
152 |
rescue OpenIdAuthentication::InvalidOpenId
|
|
153 |
# Invlaid url, don't save
|
|
154 |
end
|
|
155 |
end
|
|
156 |
self.read_attribute(:identity_url)
|
|
157 |
end
|
|
158 |
|
|
159 |
# Returns the user that matches provided login and password, or nil
|
|
160 |
def self.try_to_login(login, password)
|
|
161 |
login = login.to_s
|
|
162 |
password = password.to_s
|
|
163 |
|
|
164 |
# Make sure no one can sign in with an empty password
|
|
165 |
return nil if password.empty?
|
|
166 |
user = find_by_login(login)
|
|
167 |
if user
|
|
168 |
# user is already in local database
|
|
169 |
return nil if !user.active?
|
|
170 |
if user.auth_source
|
|
171 |
# user has an external authentication method
|
|
172 |
return nil unless user.auth_source.authenticate(login, password)
|
|
173 |
else
|
|
174 |
# authentication with local password
|
|
175 |
return nil unless user.check_password?(password)
|
|
176 |
end
|
|
177 |
else
|
|
178 |
# user is not yet registered, try to authenticate with available sources
|
|
179 |
attrs = AuthSource.authenticate(login, password)
|
|
180 |
if attrs
|
|
181 |
user = new(attrs)
|
|
182 |
user.login = login
|
|
183 |
user.language = Setting.default_language
|
|
184 |
if user.save
|
|
185 |
user.reload
|
|
186 |
logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
|
|
187 |
end
|
|
188 |
end
|
|
189 |
end
|
|
190 |
user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
|
|
191 |
user
|
|
192 |
rescue => text
|
|
193 |
raise text
|
|
194 |
end
|
|
195 |
|
|
196 |
# Returns the user who matches the given autologin +key+ or nil
|
|
197 |
def self.try_to_autologin(key)
|
|
198 |
tokens = Token.find_all_by_action_and_value('autologin', key.to_s)
|
|
199 |
# Make sure there's only 1 token that matches the key
|
|
200 |
if tokens.size == 1
|
|
201 |
token = tokens.first
|
|
202 |
if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
|
|
203 |
token.user.update_attribute(:last_login_on, Time.now)
|
|
204 |
token.user
|
|
205 |
end
|
|
206 |
end
|
|
207 |
end
|
|
208 |
|
|
209 |
def self.name_formatter(formatter = nil)
|
|
210 |
USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
|
|
211 |
end
|
|
212 |
|
|
213 |
# Returns an array of fields names than can be used to make an order statement for users
|
|
214 |
# according to how user names are displayed
|
|
215 |
# Examples:
|
|
216 |
#
|
|
217 |
# User.fields_for_order_statement => ['users.login', 'users.id']
|
|
218 |
# User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
|
|
219 |
def self.fields_for_order_statement(table=nil)
|
|
220 |
table ||= table_name
|
|
221 |
name_formatter[:order].map {|field| "#{table}.#{field}"}
|
|
222 |
end
|
|
223 |
|
|
224 |
# Return user's full name for display
|
|
225 |
def name(formatter = nil)
|
|
226 |
f = self.class.name_formatter(formatter)
|
|
227 |
if formatter
|
|
228 |
eval('"' + f[:string] + '"')
|
|
229 |
else
|
|
230 |
@name ||= eval('"' + f[:string] + '"')
|
|
231 |
end
|
|
232 |
end
|
|
233 |
|
|
234 |
def active?
|
|
235 |
self.status == STATUS_ACTIVE
|
|
236 |
end
|
|
237 |
|
|
238 |
def registered?
|
|
239 |
self.status == STATUS_REGISTERED
|
|
240 |
end
|
|
241 |
|
|
242 |
def locked?
|
|
243 |
self.status == STATUS_LOCKED
|
|
244 |
end
|
|
245 |
|
|
246 |
def activate
|
|
247 |
self.status = STATUS_ACTIVE
|
|
248 |
end
|
|
249 |
|
|
250 |
def register
|
|
251 |
self.status = STATUS_REGISTERED
|
|
252 |
end
|
|
253 |
|
|
254 |
def lock
|
|
255 |
self.status = STATUS_LOCKED
|
|
256 |
end
|
|
257 |
|
|
258 |
def activate!
|
|
259 |
update_attribute(:status, STATUS_ACTIVE)
|
|
260 |
end
|
|
261 |
|
|
262 |
def register!
|
|
263 |
update_attribute(:status, STATUS_REGISTERED)
|
|
264 |
end
|
|
265 |
|
|
266 |
def lock!
|
|
267 |
update_attribute(:status, STATUS_LOCKED)
|
|
268 |
end
|
|
269 |
|
|
270 |
# Returns true if +clear_password+ is the correct user's password, otherwise false
|
|
271 |
def check_password?(clear_password)
|
|
272 |
if auth_source_id.present?
|
|
273 |
auth_source.authenticate(self.login, clear_password)
|
|
274 |
else
|
|
275 |
User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
|
|
276 |
end
|
|
277 |
end
|
|
278 |
|
|
279 |
# Generates a random salt and computes hashed_password for +clear_password+
|
|
280 |
# The hashed password is stored in the following form: SHA1(salt + SHA1(password))
|
|
281 |
def salt_password(clear_password)
|
|
282 |
self.salt = User.generate_salt
|
|
283 |
self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
|
|
284 |
end
|
|
285 |
|
|
286 |
# Does the backend storage allow this user to change their password?
|
|
287 |
def change_password_allowed?
|
|
288 |
return true if auth_source.nil?
|
|
289 |
return auth_source.allow_password_changes?
|
|
290 |
end
|
|
291 |
|
|
292 |
# Generate and set a random password. Useful for automated user creation
|
|
293 |
# Based on Token#generate_token_value
|
|
294 |
#
|
|
295 |
def random_password
|
|
296 |
chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
|
|
297 |
password = ''
|
|
298 |
40.times { |i| password << chars[rand(chars.size-1)] }
|
|
299 |
self.password = password
|
|
300 |
self.password_confirmation = password
|
|
301 |
self
|
|
302 |
end
|
|
303 |
|
|
304 |
def pref
|
|
305 |
self.preference ||= UserPreference.new(:user => self)
|
|
306 |
end
|
|
307 |
|
|
308 |
def time_zone
|
|
309 |
@time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
|
|
310 |
end
|
|
311 |
|
|
312 |
def wants_comments_in_reverse_order?
|
|
313 |
self.pref[:comments_sorting] == 'desc'
|
|
314 |
end
|
|
315 |
|
|
316 |
# Return user's RSS key (a 40 chars long string), used to access feeds
|
|
317 |
def rss_key
|
|
318 |
if rss_token.nil?
|
|
319 |
create_rss_token(:action => 'feeds')
|
|
320 |
end
|
|
321 |
rss_token.value
|
|
322 |
end
|
|
323 |
|
|
324 |
# Return user's API key (a 40 chars long string), used to access the API
|
|
325 |
def api_key
|
|
326 |
if api_token.nil?
|
|
327 |
create_api_token(:action => 'api')
|
|
328 |
end
|
|
329 |
api_token.value
|
|
330 |
end
|
|
331 |
|
|
332 |
# Return an array of project ids for which the user has explicitly turned mail notifications on
|
|
333 |
def notified_projects_ids
|
|
334 |
@notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
|
|
335 |
end
|
|
336 |
|
|
337 |
def notified_project_ids=(ids)
|
|
338 |
Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
|
|
339 |
Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
|
|
340 |
@notified_projects_ids = nil
|
|
341 |
notified_projects_ids
|
|
342 |
end
|
|
343 |
|
|
344 |
def valid_notification_options
|
|
345 |
self.class.valid_notification_options(self)
|
|
346 |
end
|
|
347 |
|
|
348 |
# Only users that belong to more than 1 project can select projects for which they are notified
|
|
349 |
def self.valid_notification_options(user=nil)
|
|
350 |
# Note that @user.membership.size would fail since AR ignores
|
|
351 |
# :include association option when doing a count
|
|
352 |
if user.nil? || user.memberships.length < 1
|
|
353 |
MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
|
|
354 |
else
|
|
355 |
MAIL_NOTIFICATION_OPTIONS
|
|
356 |
end
|
|
357 |
end
|
|
358 |
|
|
359 |
# Find a user account by matching the exact login and then a case-insensitive
|
|
360 |
# version. Exact matches will be given priority.
|
|
361 |
def self.find_by_login(login)
|
|
362 |
# First look for an exact match
|
|
363 |
user = where(:login => login).all.detect {|u| u.login == login}
|
|
364 |
unless user
|
|
365 |
# Fail over to case-insensitive if none was found
|
|
366 |
user = where("LOWER(login) = ?", login.to_s.downcase).first
|
|
367 |
end
|
|
368 |
user
|
|
369 |
end
|
|
370 |
|
|
371 |
def self.find_by_rss_key(key)
|
|
372 |
token = Token.find_by_action_and_value('feeds', key.to_s)
|
|
373 |
token && token.user.active? ? token.user : nil
|
|
374 |
end
|
|
375 |
|
|
376 |
def self.find_by_api_key(key)
|
|
377 |
token = Token.find_by_action_and_value('api', key.to_s)
|
|
378 |
token && token.user.active? ? token.user : nil
|
|
379 |
end
|
|
380 |
|
|
381 |
# Makes find_by_mail case-insensitive
|
|
382 |
def self.find_by_mail(mail)
|
|
383 |
where("LOWER(mail) = ?", mail.to_s.downcase).first
|
|
384 |
end
|
|
385 |
|
|
386 |
# Returns true if the default admin account can no longer be used
|
|
387 |
def self.default_admin_account_changed?
|
|
388 |
!User.active.find_by_login("admin").try(:check_password?, "admin")
|
|
389 |
end
|
|
390 |
|
|
391 |
def to_s
|
|
392 |
name
|
|
393 |
end
|
|
394 |
|
|
395 |
CSS_CLASS_BY_STATUS = {
|
|
396 |
STATUS_ANONYMOUS => 'anon',
|
|
397 |
STATUS_ACTIVE => 'active',
|
|
398 |
STATUS_REGISTERED => 'registered',
|
|
399 |
STATUS_LOCKED => 'locked'
|
|
400 |
}
|
|
401 |
|
|
402 |
def css_classes
|
|
403 |
"user #{CSS_CLASS_BY_STATUS[status]}"
|
|
404 |
end
|
|
405 |
|
|
406 |
# Returns the current day according to user's time zone
|
|
407 |
def today
|
|
408 |
if time_zone.nil?
|
|
409 |
Date.today
|
|
410 |
else
|
|
411 |
Time.now.in_time_zone(time_zone).to_date
|
|
412 |
end
|
|
413 |
end
|
|
414 |
|
|
415 |
# Returns the day of +time+ according to user's time zone
|
|
416 |
def time_to_date(time)
|
|
417 |
if time_zone.nil?
|
|
418 |
time.to_date
|
|
419 |
else
|
|
420 |
time.in_time_zone(time_zone).to_date
|
|
421 |
end
|
|
422 |
end
|
|
423 |
|
|
424 |
def logged?
|
|
425 |
true
|
|
426 |
end
|
|
427 |
|
|
428 |
def anonymous?
|
|
429 |
!logged?
|
|
430 |
end
|
|
431 |
|
|
432 |
# Return user's roles for project
|
|
433 |
def roles_for_project(project)
|
|
434 |
roles = []
|
|
435 |
# No role on archived projects
|
|
436 |
return roles if project.nil? || project.archived?
|
|
437 |
if logged?
|
|
438 |
# Find project membership
|
|
439 |
membership = memberships.detect {|m| m.project_id == project.id}
|
|
440 |
if membership
|
|
441 |
roles = membership.roles
|
|
442 |
else
|
|
443 |
@role_non_member ||= Role.non_member
|
|
444 |
roles << @role_non_member
|
|
445 |
end
|
|
446 |
else
|
|
447 |
@role_anonymous ||= Role.anonymous
|
|
448 |
roles << @role_anonymous
|
|
449 |
end
|
|
450 |
roles
|
|
451 |
end
|
|
452 |
|
|
453 |
# Return true if the user is a member of project
|
|
454 |
def member_of?(project)
|
|
455 |
!roles_for_project(project).detect {|role| role.member?}.nil?
|
|
456 |
end
|
|
457 |
|
|
458 |
# Returns a hash of user's projects grouped by roles
|
|
459 |
def projects_by_role
|
|
460 |
return @projects_by_role if @projects_by_role
|
|
461 |
|
|
462 |
@projects_by_role = Hash.new([])
|
|
463 |
memberships.each do |membership|
|
|
464 |
if membership.project
|
|
465 |
membership.roles.each do |role|
|
|
466 |
@projects_by_role[role] = [] unless @projects_by_role.key?(role)
|
|
467 |
@projects_by_role[role] << membership.project
|
|
468 |
end
|
|
469 |
end
|
|
470 |
end
|
|
471 |
@projects_by_role.each do |role, projects|
|
|
472 |
projects.uniq!
|
|
473 |
end
|
|
474 |
|
|
475 |
@projects_by_role
|
|
476 |
end
|
|
477 |
|
|
478 |
# Returns true if user is arg or belongs to arg
|
|
479 |
def is_or_belongs_to?(arg)
|
|
480 |
if arg.is_a?(User)
|
|
481 |
self == arg
|
|
482 |
elsif arg.is_a?(Group)
|
|
483 |
arg.users.include?(self)
|
|
484 |
else
|
|
485 |
false
|
|
486 |
end
|
|
487 |
end
|
|
488 |
|
|
489 |
# Return true if the user is allowed to do the specified action on a specific context
|
|
490 |
# Action can be:
|
|
491 |
# * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
|
|
492 |
# * a permission Symbol (eg. :edit_project)
|
|
493 |
# Context can be:
|
|
494 |
# * a project : returns true if user is allowed to do the specified action on this project
|
|
495 |
# * an array of projects : returns true if user is allowed on every project
|
|
496 |
# * nil with options[:global] set : check if user has at least one role allowed for this action,
|
|
497 |
# or falls back to Non Member / Anonymous permissions depending if the user is logged
|
|
498 |
def allowed_to?(action, context, options={}, &block)
|
|
499 |
if context && context.is_a?(Project)
|
|
500 |
return false unless context.allows_to?(action)
|
|
501 |
# Admin users are authorized for anything else
|
|
502 |
return true if admin?
|
|
503 |
|
|
504 |
roles = roles_for_project(context)
|
|
505 |
return false unless roles
|
|
506 |
roles.any? {|role|
|
|
507 |
(context.is_public? || role.member?) &&
|
|
508 |
role.allowed_to?(action) &&
|
|
509 |
(block_given? ? yield(role, self) : true)
|
|
510 |
}
|
|
511 |
elsif context && context.is_a?(Array)
|
|
512 |
if context.empty?
|
|
513 |
false
|
|
514 |
else
|
|
515 |
# Authorize if user is authorized on every element of the array
|
|
516 |
context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
|
|
517 |
end
|
|
518 |
elsif options[:global]
|
|
519 |
# Admin users are always authorized
|
|
520 |
return true if admin?
|
|
521 |
|
|
522 |
# authorize if user has at least one role that has this permission
|
|
523 |
roles = memberships.collect {|m| m.roles}.flatten.uniq
|
|
524 |
roles << (self.logged? ? Role.non_member : Role.anonymous)
|
|
525 |
roles.any? {|role|
|
|
526 |
role.allowed_to?(action) &&
|
|
527 |
(block_given? ? yield(role, self) : true)
|
|
528 |
}
|
|
529 |
else
|
|
530 |
false
|
|
531 |
end
|
|
532 |
end
|
|
533 |
|
|
534 |
# Is the user allowed to do the specified action on any project?
|
|
535 |
# See allowed_to? for the actions and valid options.
|
|
536 |
def allowed_to_globally?(action, options, &block)
|
|
537 |
allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
|
|
538 |
end
|
|
539 |
|
|
540 |
# Returns true if the user is allowed to delete his own account
|
|
541 |
def own_account_deletable?
|
|
542 |
Setting.unsubscribe? &&
|
|
543 |
(!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
|
|
544 |
end
|
|
545 |
|
|
546 |
safe_attributes 'login',
|
|
547 |
'firstname',
|
|
548 |
'lastname',
|
|
549 |
'mail',
|
|
550 |
'mail_notification',
|
|
551 |
'language',
|
|
552 |
'custom_field_values',
|
|
553 |
'custom_fields',
|
|
554 |
'identity_url'
|
|
555 |
|
|
556 |
safe_attributes 'status',
|
|
557 |
'auth_source_id',
|
|
558 |
:if => lambda {|user, current_user| current_user.admin?}
|
|
559 |
|
|
560 |
safe_attributes 'group_ids',
|
|
561 |
:if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
|
|
562 |
|
|
563 |
# Utility method to help check if a user should be notified about an
|
|
564 |
# event.
|
|
565 |
#
|
|
566 |
# TODO: only supports Issue events currently
|
|
567 |
def notify_about?(object)
|
|
568 |
case mail_notification
|
|
569 |
when 'all'
|
|
570 |
true
|
|
571 |
when 'selected'
|
|
572 |
# user receives notifications for created/assigned issues on unselected projects
|
|
573 |
if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
|
|
574 |
true
|
|
575 |
else
|
|
576 |
false
|
|
577 |
end
|
|
578 |
when 'none'
|
|
579 |
false
|
|
580 |
when 'only_my_events'
|
|
581 |
if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
|
|
582 |
true
|
|
583 |
else
|
|
584 |
false
|
|
585 |
end
|
|
586 |
when 'only_assigned'
|
|
587 |
if object.is_a?(Issue) && (is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
|
|
588 |
true
|
|
589 |
else
|
|
590 |
false
|
|
591 |
end
|
|
592 |
when 'only_owner'
|
|
593 |
if object.is_a?(Issue) && object.author == self
|
|
594 |
true
|
|
595 |
else
|
|
596 |
false
|
|
597 |
end
|
|
598 |
else
|
|
599 |
false
|
|
600 |
end
|
|
601 |
end
|
|
602 |
|
|
603 |
def self.current=(user)
|
|
604 |
@current_user = user
|
|
605 |
end
|
|
606 |
|
|
607 |
def self.current
|
|
608 |
@current_user ||= User.anonymous
|
|
609 |
end
|
|
610 |
|
|
611 |
# Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
|
|
612 |
# one anonymous user per database.
|
|
613 |
def self.anonymous
|
|
614 |
anonymous_user = AnonymousUser.first
|
|
615 |
if anonymous_user.nil?
|
|
616 |
anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
|
|
617 |
raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
|
|
618 |
end
|
|
619 |
anonymous_user
|
|
620 |
end
|
|
621 |
|
|
622 |
# Salts all existing unsalted passwords
|
|
623 |
# It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
|
|
624 |
# This method is used in the SaltPasswords migration and is to be kept as is
|
|
625 |
def self.salt_unsalted_passwords!
|
|
626 |
transaction do
|
|
627 |
User.where("salt IS NULL OR salt = ''").find_each do |user|
|
|
628 |
next if user.hashed_password.blank?
|
|
629 |
salt = User.generate_salt
|
|
630 |
hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
|
|
631 |
User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
|
|
632 |
end
|
|
633 |
end
|
|
634 |
end
|
|
635 |
|
|
636 |
protected
|
|
637 |
|
|
638 |
def validate_password_length
|
|
639 |
# Password length validation based on setting
|
|
640 |
if !password.nil? && password.size < Setting.password_min_length.to_i
|
|
641 |
errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
|
|
642 |
end
|
|
643 |
end
|
|
644 |
|
|
645 |
private
|
|
646 |
|
|
647 |
# Removes references that are not handled by associations
|
|
648 |
# Things that are not deleted are reassociated with the anonymous user
|
|
649 |
def remove_references_before_destroy
|
|
650 |
return if self.id.nil?
|
|
651 |
|
|
652 |
substitute = User.anonymous
|
|
653 |
Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
|
|
654 |
Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
|
|
655 |
Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
|
|
656 |
Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
|
|
657 |
Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
|
|
658 |
JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
|
|
659 |
JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
|
|
660 |
Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
|
|
661 |
News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
|
|
662 |
# Remove private queries and keep public ones
|
|
663 |
::Query.delete_all ['user_id = ? AND is_public = ?', id, false]
|
|
664 |
::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
|
|
665 |
TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
|
|
666 |
Token.delete_all ['user_id = ?', id]
|
|
667 |
Watcher.delete_all ['user_id = ?', id]
|
|
668 |
WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
|
|
669 |
WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
|
|
670 |
end
|
|
671 |
|
|
672 |
# Return password digest
|
|
673 |
def self.hash_password(clear_password)
|
|
674 |
Digest::SHA1.hexdigest(clear_password || "")
|
|
675 |
end
|
|
676 |
|
|
677 |
# Returns a 128bits random salt as a hex string (32 chars long)
|
|
678 |
def self.generate_salt
|
|
679 |
Redmine::Utils.random_hex(16)
|
|
680 |
end
|
|
681 |
|
|
682 |
end
|
|
683 |
|
|
684 |
class AnonymousUser < User
|
|
685 |
validate :validate_anonymous_uniqueness, :on => :create
|
|
686 |
|
|
687 |
def validate_anonymous_uniqueness
|
|
688 |
# There should be only one AnonymousUser in the database
|
|
689 |
errors.add :base, 'An anonymous user already exists.' if AnonymousUser.find(:first)
|
|
690 |
end
|
|
691 |
|
|
692 |
def available_custom_fields
|
|
693 |
[]
|
|
694 |
end
|
|
695 |
|
|
696 |
# Overrides a few properties
|
|
697 |
def logged?; false end
|
|
698 |
def admin; false end
|
|
699 |
def name(*args); I18n.t(:label_user_anonymous) end
|
|
700 |
def mail; nil end
|
|
701 |
def time_zone; nil end
|
|
702 |
def rss_key; nil end
|
|
703 |
|
|
704 |
def pref
|
|
705 |
UserPreference.new(:user => self)
|
|
706 |
end
|
|
707 |
|
|
708 |
# Anonymous user can not be destroyed
|
|
709 |
def destroy
|
|
710 |
false
|
|
711 |
end
|
|
712 |
end
|