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