##// END OF EJS Templates
Send a security notification when users gain or loose admin (#21421)....
Jean-Philippe Lang -
r14883:4aef2735c878
parent child
Show More
@@ -1,888 +1,917
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 # Different ways of displaying/sorting users
23 # Different ways of displaying/sorting users
24 USER_FORMATS = {
24 USER_FORMATS = {
25 :firstname_lastname => {
25 :firstname_lastname => {
26 :string => '#{firstname} #{lastname}',
26 :string => '#{firstname} #{lastname}',
27 :order => %w(firstname lastname id),
27 :order => %w(firstname lastname id),
28 :setting_order => 1
28 :setting_order => 1
29 },
29 },
30 :firstname_lastinitial => {
30 :firstname_lastinitial => {
31 :string => '#{firstname} #{lastname.to_s.chars.first}.',
31 :string => '#{firstname} #{lastname.to_s.chars.first}.',
32 :order => %w(firstname lastname id),
32 :order => %w(firstname lastname id),
33 :setting_order => 2
33 :setting_order => 2
34 },
34 },
35 :firstinitial_lastname => {
35 :firstinitial_lastname => {
36 :string => '#{firstname.to_s.gsub(/(([[:alpha:]])[[:alpha:]]*\.?)/, \'\2.\')} #{lastname}',
36 :string => '#{firstname.to_s.gsub(/(([[:alpha:]])[[:alpha:]]*\.?)/, \'\2.\')} #{lastname}',
37 :order => %w(firstname lastname id),
37 :order => %w(firstname lastname id),
38 :setting_order => 2
38 :setting_order => 2
39 },
39 },
40 :firstname => {
40 :firstname => {
41 :string => '#{firstname}',
41 :string => '#{firstname}',
42 :order => %w(firstname id),
42 :order => %w(firstname id),
43 :setting_order => 3
43 :setting_order => 3
44 },
44 },
45 :lastname_firstname => {
45 :lastname_firstname => {
46 :string => '#{lastname} #{firstname}',
46 :string => '#{lastname} #{firstname}',
47 :order => %w(lastname firstname id),
47 :order => %w(lastname firstname id),
48 :setting_order => 4
48 :setting_order => 4
49 },
49 },
50 :lastnamefirstname => {
50 :lastnamefirstname => {
51 :string => '#{lastname}#{firstname}',
51 :string => '#{lastname}#{firstname}',
52 :order => %w(lastname firstname id),
52 :order => %w(lastname firstname id),
53 :setting_order => 5
53 :setting_order => 5
54 },
54 },
55 :lastname_comma_firstname => {
55 :lastname_comma_firstname => {
56 :string => '#{lastname}, #{firstname}',
56 :string => '#{lastname}, #{firstname}',
57 :order => %w(lastname firstname id),
57 :order => %w(lastname firstname id),
58 :setting_order => 6
58 :setting_order => 6
59 },
59 },
60 :lastname => {
60 :lastname => {
61 :string => '#{lastname}',
61 :string => '#{lastname}',
62 :order => %w(lastname id),
62 :order => %w(lastname id),
63 :setting_order => 7
63 :setting_order => 7
64 },
64 },
65 :username => {
65 :username => {
66 :string => '#{login}',
66 :string => '#{login}',
67 :order => %w(login id),
67 :order => %w(login id),
68 :setting_order => 8
68 :setting_order => 8
69 },
69 },
70 }
70 }
71
71
72 MAIL_NOTIFICATION_OPTIONS = [
72 MAIL_NOTIFICATION_OPTIONS = [
73 ['all', :label_user_mail_option_all],
73 ['all', :label_user_mail_option_all],
74 ['selected', :label_user_mail_option_selected],
74 ['selected', :label_user_mail_option_selected],
75 ['only_my_events', :label_user_mail_option_only_my_events],
75 ['only_my_events', :label_user_mail_option_only_my_events],
76 ['only_assigned', :label_user_mail_option_only_assigned],
76 ['only_assigned', :label_user_mail_option_only_assigned],
77 ['only_owner', :label_user_mail_option_only_owner],
77 ['only_owner', :label_user_mail_option_only_owner],
78 ['none', :label_user_mail_option_none]
78 ['none', :label_user_mail_option_none]
79 ]
79 ]
80
80
81 has_and_belongs_to_many :groups,
81 has_and_belongs_to_many :groups,
82 :join_table => "#{table_name_prefix}groups_users#{table_name_suffix}",
82 :join_table => "#{table_name_prefix}groups_users#{table_name_suffix}",
83 :after_add => Proc.new {|user, group| group.user_added(user)},
83 :after_add => Proc.new {|user, group| group.user_added(user)},
84 :after_remove => Proc.new {|user, group| group.user_removed(user)}
84 :after_remove => Proc.new {|user, group| group.user_removed(user)}
85 has_many :changesets, :dependent => :nullify
85 has_many :changesets, :dependent => :nullify
86 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
86 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
87 has_one :rss_token, lambda {where "action='feeds'"}, :class_name => 'Token'
87 has_one :rss_token, lambda {where "action='feeds'"}, :class_name => 'Token'
88 has_one :api_token, lambda {where "action='api'"}, :class_name => 'Token'
88 has_one :api_token, lambda {where "action='api'"}, :class_name => 'Token'
89 has_one :email_address, lambda {where :is_default => true}, :autosave => true
89 has_one :email_address, lambda {where :is_default => true}, :autosave => true
90 has_many :email_addresses, :dependent => :delete_all
90 has_many :email_addresses, :dependent => :delete_all
91 belongs_to :auth_source
91 belongs_to :auth_source
92
92
93 scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
93 scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
94 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
94 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
95
95
96 acts_as_customizable
96 acts_as_customizable
97
97
98 attr_accessor :password, :password_confirmation, :generate_password
98 attr_accessor :password, :password_confirmation, :generate_password
99 attr_accessor :last_before_login_on
99 attr_accessor :last_before_login_on
100 attr_accessor :remote_ip
100 attr_accessor :remote_ip
101
101
102 # Prevents unauthorized assignments
102 # Prevents unauthorized assignments
103 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
103 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
104
104
105 LOGIN_LENGTH_LIMIT = 60
105 LOGIN_LENGTH_LIMIT = 60
106 MAIL_LENGTH_LIMIT = 60
106 MAIL_LENGTH_LIMIT = 60
107
107
108 validates_presence_of :login, :firstname, :lastname, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
108 validates_presence_of :login, :firstname, :lastname, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
109 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
109 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
110 # Login must contain letters, numbers, underscores only
110 # Login must contain letters, numbers, underscores only
111 validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i
111 validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i
112 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
112 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
113 validates_length_of :firstname, :lastname, :maximum => 30
113 validates_length_of :firstname, :lastname, :maximum => 30
114 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
114 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
115 validate :validate_password_length
115 validate :validate_password_length
116 validate do
116 validate do
117 if password_confirmation && password != password_confirmation
117 if password_confirmation && password != password_confirmation
118 errors.add(:password, :confirmation)
118 errors.add(:password, :confirmation)
119 end
119 end
120 end
120 end
121
121
122 before_validation :instantiate_email_address
122 before_validation :instantiate_email_address
123 before_create :set_mail_notification
123 before_create :set_mail_notification
124 before_save :generate_password_if_needed, :update_hashed_password
124 before_save :generate_password_if_needed, :update_hashed_password
125 before_destroy :remove_references_before_destroy
125 before_destroy :remove_references_before_destroy
126 after_save :update_notified_project_ids, :destroy_tokens
126 after_save :update_notified_project_ids, :destroy_tokens, :deliver_security_notification
127 after_destroy :deliver_security_notification
127
128
128 scope :in_group, lambda {|group|
129 scope :in_group, lambda {|group|
129 group_id = group.is_a?(Group) ? group.id : group.to_i
130 group_id = group.is_a?(Group) ? group.id : group.to_i
130 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)
131 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)
131 }
132 }
132 scope :not_in_group, lambda {|group|
133 scope :not_in_group, lambda {|group|
133 group_id = group.is_a?(Group) ? group.id : group.to_i
134 group_id = group.is_a?(Group) ? group.id : group.to_i
134 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)
135 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)
135 }
136 }
136 scope :sorted, lambda { order(*User.fields_for_order_statement)}
137 scope :sorted, lambda { order(*User.fields_for_order_statement)}
137 scope :having_mail, lambda {|arg|
138 scope :having_mail, lambda {|arg|
138 addresses = Array.wrap(arg).map {|a| a.to_s.downcase}
139 addresses = Array.wrap(arg).map {|a| a.to_s.downcase}
139 if addresses.any?
140 if addresses.any?
140 joins(:email_addresses).where("LOWER(#{EmailAddress.table_name}.address) IN (?)", addresses).uniq
141 joins(:email_addresses).where("LOWER(#{EmailAddress.table_name}.address) IN (?)", addresses).uniq
141 else
142 else
142 none
143 none
143 end
144 end
144 }
145 }
145
146
146 def set_mail_notification
147 def set_mail_notification
147 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
148 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
148 true
149 true
149 end
150 end
150
151
151 def update_hashed_password
152 def update_hashed_password
152 # update hashed_password if password was set
153 # update hashed_password if password was set
153 if self.password && self.auth_source_id.blank?
154 if self.password && self.auth_source_id.blank?
154 salt_password(password)
155 salt_password(password)
155 end
156 end
156 end
157 end
157
158
158 alias :base_reload :reload
159 alias :base_reload :reload
159 def reload(*args)
160 def reload(*args)
160 @name = nil
161 @name = nil
161 @projects_by_role = nil
162 @projects_by_role = nil
162 @membership_by_project_id = nil
163 @membership_by_project_id = nil
163 @notified_projects_ids = nil
164 @notified_projects_ids = nil
164 @notified_projects_ids_changed = false
165 @notified_projects_ids_changed = false
165 @builtin_role = nil
166 @builtin_role = nil
166 @visible_project_ids = nil
167 @visible_project_ids = nil
167 @managed_roles = nil
168 @managed_roles = nil
168 base_reload(*args)
169 base_reload(*args)
169 end
170 end
170
171
171 def mail
172 def mail
172 email_address.try(:address)
173 email_address.try(:address)
173 end
174 end
174
175
175 def mail=(arg)
176 def mail=(arg)
176 email = email_address || build_email_address
177 email = email_address || build_email_address
177 email.address = arg
178 email.address = arg
178 end
179 end
179
180
180 def mail_changed?
181 def mail_changed?
181 email_address.try(:address_changed?)
182 email_address.try(:address_changed?)
182 end
183 end
183
184
184 def mails
185 def mails
185 email_addresses.pluck(:address)
186 email_addresses.pluck(:address)
186 end
187 end
187
188
188 def self.find_or_initialize_by_identity_url(url)
189 def self.find_or_initialize_by_identity_url(url)
189 user = where(:identity_url => url).first
190 user = where(:identity_url => url).first
190 unless user
191 unless user
191 user = User.new
192 user = User.new
192 user.identity_url = url
193 user.identity_url = url
193 end
194 end
194 user
195 user
195 end
196 end
196
197
197 def identity_url=(url)
198 def identity_url=(url)
198 if url.blank?
199 if url.blank?
199 write_attribute(:identity_url, '')
200 write_attribute(:identity_url, '')
200 else
201 else
201 begin
202 begin
202 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
203 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
203 rescue OpenIdAuthentication::InvalidOpenId
204 rescue OpenIdAuthentication::InvalidOpenId
204 # Invalid url, don't save
205 # Invalid url, don't save
205 end
206 end
206 end
207 end
207 self.read_attribute(:identity_url)
208 self.read_attribute(:identity_url)
208 end
209 end
209
210
210 # Returns the user that matches provided login and password, or nil
211 # Returns the user that matches provided login and password, or nil
211 def self.try_to_login(login, password, active_only=true)
212 def self.try_to_login(login, password, active_only=true)
212 login = login.to_s
213 login = login.to_s
213 password = password.to_s
214 password = password.to_s
214
215
215 # Make sure no one can sign in with an empty login or password
216 # Make sure no one can sign in with an empty login or password
216 return nil if login.empty? || password.empty?
217 return nil if login.empty? || password.empty?
217 user = find_by_login(login)
218 user = find_by_login(login)
218 if user
219 if user
219 # user is already in local database
220 # user is already in local database
220 return nil unless user.check_password?(password)
221 return nil unless user.check_password?(password)
221 return nil if !user.active? && active_only
222 return nil if !user.active? && active_only
222 else
223 else
223 # user is not yet registered, try to authenticate with available sources
224 # user is not yet registered, try to authenticate with available sources
224 attrs = AuthSource.authenticate(login, password)
225 attrs = AuthSource.authenticate(login, password)
225 if attrs
226 if attrs
226 user = new(attrs)
227 user = new(attrs)
227 user.login = login
228 user.login = login
228 user.language = Setting.default_language
229 user.language = Setting.default_language
229 if user.save
230 if user.save
230 user.reload
231 user.reload
231 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
232 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
232 end
233 end
233 end
234 end
234 end
235 end
235 user.update_column(:last_login_on, Time.now) if user && !user.new_record? && user.active?
236 user.update_column(:last_login_on, Time.now) if user && !user.new_record? && user.active?
236 user
237 user
237 rescue => text
238 rescue => text
238 raise text
239 raise text
239 end
240 end
240
241
241 # Returns the user who matches the given autologin +key+ or nil
242 # Returns the user who matches the given autologin +key+ or nil
242 def self.try_to_autologin(key)
243 def self.try_to_autologin(key)
243 user = Token.find_active_user('autologin', key, Setting.autologin.to_i)
244 user = Token.find_active_user('autologin', key, Setting.autologin.to_i)
244 if user
245 if user
245 user.update_column(:last_login_on, Time.now)
246 user.update_column(:last_login_on, Time.now)
246 user
247 user
247 end
248 end
248 end
249 end
249
250
250 def self.name_formatter(formatter = nil)
251 def self.name_formatter(formatter = nil)
251 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
252 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
252 end
253 end
253
254
254 # Returns an array of fields names than can be used to make an order statement for users
255 # Returns an array of fields names than can be used to make an order statement for users
255 # according to how user names are displayed
256 # according to how user names are displayed
256 # Examples:
257 # Examples:
257 #
258 #
258 # User.fields_for_order_statement => ['users.login', 'users.id']
259 # User.fields_for_order_statement => ['users.login', 'users.id']
259 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
260 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
260 def self.fields_for_order_statement(table=nil)
261 def self.fields_for_order_statement(table=nil)
261 table ||= table_name
262 table ||= table_name
262 name_formatter[:order].map {|field| "#{table}.#{field}"}
263 name_formatter[:order].map {|field| "#{table}.#{field}"}
263 end
264 end
264
265
265 # Return user's full name for display
266 # Return user's full name for display
266 def name(formatter = nil)
267 def name(formatter = nil)
267 f = self.class.name_formatter(formatter)
268 f = self.class.name_formatter(formatter)
268 if formatter
269 if formatter
269 eval('"' + f[:string] + '"')
270 eval('"' + f[:string] + '"')
270 else
271 else
271 @name ||= eval('"' + f[:string] + '"')
272 @name ||= eval('"' + f[:string] + '"')
272 end
273 end
273 end
274 end
274
275
275 def active?
276 def active?
276 self.status == STATUS_ACTIVE
277 self.status == STATUS_ACTIVE
277 end
278 end
278
279
279 def registered?
280 def registered?
280 self.status == STATUS_REGISTERED
281 self.status == STATUS_REGISTERED
281 end
282 end
282
283
283 def locked?
284 def locked?
284 self.status == STATUS_LOCKED
285 self.status == STATUS_LOCKED
285 end
286 end
286
287
287 def activate
288 def activate
288 self.status = STATUS_ACTIVE
289 self.status = STATUS_ACTIVE
289 end
290 end
290
291
291 def register
292 def register
292 self.status = STATUS_REGISTERED
293 self.status = STATUS_REGISTERED
293 end
294 end
294
295
295 def lock
296 def lock
296 self.status = STATUS_LOCKED
297 self.status = STATUS_LOCKED
297 end
298 end
298
299
299 def activate!
300 def activate!
300 update_attribute(:status, STATUS_ACTIVE)
301 update_attribute(:status, STATUS_ACTIVE)
301 end
302 end
302
303
303 def register!
304 def register!
304 update_attribute(:status, STATUS_REGISTERED)
305 update_attribute(:status, STATUS_REGISTERED)
305 end
306 end
306
307
307 def lock!
308 def lock!
308 update_attribute(:status, STATUS_LOCKED)
309 update_attribute(:status, STATUS_LOCKED)
309 end
310 end
310
311
311 # Returns true if +clear_password+ is the correct user's password, otherwise false
312 # Returns true if +clear_password+ is the correct user's password, otherwise false
312 def check_password?(clear_password)
313 def check_password?(clear_password)
313 if auth_source_id.present?
314 if auth_source_id.present?
314 auth_source.authenticate(self.login, clear_password)
315 auth_source.authenticate(self.login, clear_password)
315 else
316 else
316 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
317 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
317 end
318 end
318 end
319 end
319
320
320 # Generates a random salt and computes hashed_password for +clear_password+
321 # Generates a random salt and computes hashed_password for +clear_password+
321 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
322 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
322 def salt_password(clear_password)
323 def salt_password(clear_password)
323 self.salt = User.generate_salt
324 self.salt = User.generate_salt
324 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
325 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
325 self.passwd_changed_on = Time.now.change(:usec => 0)
326 self.passwd_changed_on = Time.now.change(:usec => 0)
326 end
327 end
327
328
328 # Does the backend storage allow this user to change their password?
329 # Does the backend storage allow this user to change their password?
329 def change_password_allowed?
330 def change_password_allowed?
330 return true if auth_source.nil?
331 return true if auth_source.nil?
331 return auth_source.allow_password_changes?
332 return auth_source.allow_password_changes?
332 end
333 end
333
334
334 # Returns true if the user password has expired
335 # Returns true if the user password has expired
335 def password_expired?
336 def password_expired?
336 period = Setting.password_max_age.to_i
337 period = Setting.password_max_age.to_i
337 if period.zero?
338 if period.zero?
338 false
339 false
339 else
340 else
340 changed_on = self.passwd_changed_on || Time.at(0)
341 changed_on = self.passwd_changed_on || Time.at(0)
341 changed_on < period.days.ago
342 changed_on < period.days.ago
342 end
343 end
343 end
344 end
344
345
345 def must_change_password?
346 def must_change_password?
346 (must_change_passwd? || password_expired?) && change_password_allowed?
347 (must_change_passwd? || password_expired?) && change_password_allowed?
347 end
348 end
348
349
349 def generate_password?
350 def generate_password?
350 generate_password == '1' || generate_password == true
351 generate_password == '1' || generate_password == true
351 end
352 end
352
353
353 # Generate and set a random password on given length
354 # Generate and set a random password on given length
354 def random_password(length=40)
355 def random_password(length=40)
355 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
356 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
356 chars -= %w(0 O 1 l)
357 chars -= %w(0 O 1 l)
357 password = ''
358 password = ''
358 length.times {|i| password << chars[SecureRandom.random_number(chars.size)] }
359 length.times {|i| password << chars[SecureRandom.random_number(chars.size)] }
359 self.password = password
360 self.password = password
360 self.password_confirmation = password
361 self.password_confirmation = password
361 self
362 self
362 end
363 end
363
364
364 def pref
365 def pref
365 self.preference ||= UserPreference.new(:user => self)
366 self.preference ||= UserPreference.new(:user => self)
366 end
367 end
367
368
368 def time_zone
369 def time_zone
369 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
370 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
370 end
371 end
371
372
372 def force_default_language?
373 def force_default_language?
373 Setting.force_default_language_for_loggedin?
374 Setting.force_default_language_for_loggedin?
374 end
375 end
375
376
376 def language
377 def language
377 if force_default_language?
378 if force_default_language?
378 Setting.default_language
379 Setting.default_language
379 else
380 else
380 super
381 super
381 end
382 end
382 end
383 end
383
384
384 def wants_comments_in_reverse_order?
385 def wants_comments_in_reverse_order?
385 self.pref[:comments_sorting] == 'desc'
386 self.pref[:comments_sorting] == 'desc'
386 end
387 end
387
388
388 # Return user's RSS key (a 40 chars long string), used to access feeds
389 # Return user's RSS key (a 40 chars long string), used to access feeds
389 def rss_key
390 def rss_key
390 if rss_token.nil?
391 if rss_token.nil?
391 create_rss_token(:action => 'feeds')
392 create_rss_token(:action => 'feeds')
392 end
393 end
393 rss_token.value
394 rss_token.value
394 end
395 end
395
396
396 # Return user's API key (a 40 chars long string), used to access the API
397 # Return user's API key (a 40 chars long string), used to access the API
397 def api_key
398 def api_key
398 if api_token.nil?
399 if api_token.nil?
399 create_api_token(:action => 'api')
400 create_api_token(:action => 'api')
400 end
401 end
401 api_token.value
402 api_token.value
402 end
403 end
403
404
404 # Generates a new session token and returns its value
405 # Generates a new session token and returns its value
405 def generate_session_token
406 def generate_session_token
406 token = Token.create!(:user_id => id, :action => 'session')
407 token = Token.create!(:user_id => id, :action => 'session')
407 token.value
408 token.value
408 end
409 end
409
410
410 # Returns true if token is a valid session token for the user whose id is user_id
411 # Returns true if token is a valid session token for the user whose id is user_id
411 def self.verify_session_token(user_id, token)
412 def self.verify_session_token(user_id, token)
412 return false if user_id.blank? || token.blank?
413 return false if user_id.blank? || token.blank?
413
414
414 scope = Token.where(:user_id => user_id, :value => token.to_s, :action => 'session')
415 scope = Token.where(:user_id => user_id, :value => token.to_s, :action => 'session')
415 if Setting.session_lifetime?
416 if Setting.session_lifetime?
416 scope = scope.where("created_on > ?", Setting.session_lifetime.to_i.minutes.ago)
417 scope = scope.where("created_on > ?", Setting.session_lifetime.to_i.minutes.ago)
417 end
418 end
418 if Setting.session_timeout?
419 if Setting.session_timeout?
419 scope = scope.where("updated_on > ?", Setting.session_timeout.to_i.minutes.ago)
420 scope = scope.where("updated_on > ?", Setting.session_timeout.to_i.minutes.ago)
420 end
421 end
421 scope.update_all(:updated_on => Time.now) == 1
422 scope.update_all(:updated_on => Time.now) == 1
422 end
423 end
423
424
424 # Return an array of project ids for which the user has explicitly turned mail notifications on
425 # Return an array of project ids for which the user has explicitly turned mail notifications on
425 def notified_projects_ids
426 def notified_projects_ids
426 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
427 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
427 end
428 end
428
429
429 def notified_project_ids=(ids)
430 def notified_project_ids=(ids)
430 @notified_projects_ids_changed = true
431 @notified_projects_ids_changed = true
431 @notified_projects_ids = ids.map(&:to_i).uniq.select {|n| n > 0}
432 @notified_projects_ids = ids.map(&:to_i).uniq.select {|n| n > 0}
432 end
433 end
433
434
434 # Updates per project notifications (after_save callback)
435 # Updates per project notifications (after_save callback)
435 def update_notified_project_ids
436 def update_notified_project_ids
436 if @notified_projects_ids_changed
437 if @notified_projects_ids_changed
437 ids = (mail_notification == 'selected' ? Array.wrap(notified_projects_ids).reject(&:blank?) : [])
438 ids = (mail_notification == 'selected' ? Array.wrap(notified_projects_ids).reject(&:blank?) : [])
438 members.update_all(:mail_notification => false)
439 members.update_all(:mail_notification => false)
439 members.where(:project_id => ids).update_all(:mail_notification => true) if ids.any?
440 members.where(:project_id => ids).update_all(:mail_notification => true) if ids.any?
440 end
441 end
441 end
442 end
442 private :update_notified_project_ids
443 private :update_notified_project_ids
443
444
444 def valid_notification_options
445 def valid_notification_options
445 self.class.valid_notification_options(self)
446 self.class.valid_notification_options(self)
446 end
447 end
447
448
448 # Only users that belong to more than 1 project can select projects for which they are notified
449 # Only users that belong to more than 1 project can select projects for which they are notified
449 def self.valid_notification_options(user=nil)
450 def self.valid_notification_options(user=nil)
450 # Note that @user.membership.size would fail since AR ignores
451 # Note that @user.membership.size would fail since AR ignores
451 # :include association option when doing a count
452 # :include association option when doing a count
452 if user.nil? || user.memberships.length < 1
453 if user.nil? || user.memberships.length < 1
453 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
454 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
454 else
455 else
455 MAIL_NOTIFICATION_OPTIONS
456 MAIL_NOTIFICATION_OPTIONS
456 end
457 end
457 end
458 end
458
459
459 # Find a user account by matching the exact login and then a case-insensitive
460 # Find a user account by matching the exact login and then a case-insensitive
460 # version. Exact matches will be given priority.
461 # version. Exact matches will be given priority.
461 def self.find_by_login(login)
462 def self.find_by_login(login)
462 login = Redmine::CodesetUtil.replace_invalid_utf8(login.to_s)
463 login = Redmine::CodesetUtil.replace_invalid_utf8(login.to_s)
463 if login.present?
464 if login.present?
464 # First look for an exact match
465 # First look for an exact match
465 user = where(:login => login).detect {|u| u.login == login}
466 user = where(:login => login).detect {|u| u.login == login}
466 unless user
467 unless user
467 # Fail over to case-insensitive if none was found
468 # Fail over to case-insensitive if none was found
468 user = where("LOWER(login) = ?", login.downcase).first
469 user = where("LOWER(login) = ?", login.downcase).first
469 end
470 end
470 user
471 user
471 end
472 end
472 end
473 end
473
474
474 def self.find_by_rss_key(key)
475 def self.find_by_rss_key(key)
475 Token.find_active_user('feeds', key)
476 Token.find_active_user('feeds', key)
476 end
477 end
477
478
478 def self.find_by_api_key(key)
479 def self.find_by_api_key(key)
479 Token.find_active_user('api', key)
480 Token.find_active_user('api', key)
480 end
481 end
481
482
482 # Makes find_by_mail case-insensitive
483 # Makes find_by_mail case-insensitive
483 def self.find_by_mail(mail)
484 def self.find_by_mail(mail)
484 having_mail(mail).first
485 having_mail(mail).first
485 end
486 end
486
487
487 # Returns true if the default admin account can no longer be used
488 # Returns true if the default admin account can no longer be used
488 def self.default_admin_account_changed?
489 def self.default_admin_account_changed?
489 !User.active.find_by_login("admin").try(:check_password?, "admin")
490 !User.active.find_by_login("admin").try(:check_password?, "admin")
490 end
491 end
491
492
492 def to_s
493 def to_s
493 name
494 name
494 end
495 end
495
496
496 CSS_CLASS_BY_STATUS = {
497 CSS_CLASS_BY_STATUS = {
497 STATUS_ANONYMOUS => 'anon',
498 STATUS_ANONYMOUS => 'anon',
498 STATUS_ACTIVE => 'active',
499 STATUS_ACTIVE => 'active',
499 STATUS_REGISTERED => 'registered',
500 STATUS_REGISTERED => 'registered',
500 STATUS_LOCKED => 'locked'
501 STATUS_LOCKED => 'locked'
501 }
502 }
502
503
503 def css_classes
504 def css_classes
504 "user #{CSS_CLASS_BY_STATUS[status]}"
505 "user #{CSS_CLASS_BY_STATUS[status]}"
505 end
506 end
506
507
507 # Returns the current day according to user's time zone
508 # Returns the current day according to user's time zone
508 def today
509 def today
509 if time_zone.nil?
510 if time_zone.nil?
510 Date.today
511 Date.today
511 else
512 else
512 Time.now.in_time_zone(time_zone).to_date
513 Time.now.in_time_zone(time_zone).to_date
513 end
514 end
514 end
515 end
515
516
516 # Returns the day of +time+ according to user's time zone
517 # Returns the day of +time+ according to user's time zone
517 def time_to_date(time)
518 def time_to_date(time)
518 if time_zone.nil?
519 if time_zone.nil?
519 time.to_date
520 time.to_date
520 else
521 else
521 time.in_time_zone(time_zone).to_date
522 time.in_time_zone(time_zone).to_date
522 end
523 end
523 end
524 end
524
525
525 def logged?
526 def logged?
526 true
527 true
527 end
528 end
528
529
529 def anonymous?
530 def anonymous?
530 !logged?
531 !logged?
531 end
532 end
532
533
533 # Returns user's membership for the given project
534 # Returns user's membership for the given project
534 # or nil if the user is not a member of project
535 # or nil if the user is not a member of project
535 def membership(project)
536 def membership(project)
536 project_id = project.is_a?(Project) ? project.id : project
537 project_id = project.is_a?(Project) ? project.id : project
537
538
538 @membership_by_project_id ||= Hash.new {|h, project_id|
539 @membership_by_project_id ||= Hash.new {|h, project_id|
539 h[project_id] = memberships.where(:project_id => project_id).first
540 h[project_id] = memberships.where(:project_id => project_id).first
540 }
541 }
541 @membership_by_project_id[project_id]
542 @membership_by_project_id[project_id]
542 end
543 end
543
544
544 # Returns the user's bult-in role
545 # Returns the user's bult-in role
545 def builtin_role
546 def builtin_role
546 @builtin_role ||= Role.non_member
547 @builtin_role ||= Role.non_member
547 end
548 end
548
549
549 # Return user's roles for project
550 # Return user's roles for project
550 def roles_for_project(project)
551 def roles_for_project(project)
551 # No role on archived projects
552 # No role on archived projects
552 return [] if project.nil? || project.archived?
553 return [] if project.nil? || project.archived?
553 if membership = membership(project)
554 if membership = membership(project)
554 membership.roles.to_a
555 membership.roles.to_a
555 elsif project.is_public?
556 elsif project.is_public?
556 project.override_roles(builtin_role)
557 project.override_roles(builtin_role)
557 else
558 else
558 []
559 []
559 end
560 end
560 end
561 end
561
562
562 # Returns a hash of user's projects grouped by roles
563 # Returns a hash of user's projects grouped by roles
563 def projects_by_role
564 def projects_by_role
564 return @projects_by_role if @projects_by_role
565 return @projects_by_role if @projects_by_role
565
566
566 hash = Hash.new([])
567 hash = Hash.new([])
567
568
568 group_class = anonymous? ? GroupAnonymous : GroupNonMember
569 group_class = anonymous? ? GroupAnonymous : GroupNonMember
569 members = Member.joins(:project, :principal).
570 members = Member.joins(:project, :principal).
570 where("#{Project.table_name}.status <> 9").
571 where("#{Project.table_name}.status <> 9").
571 where("#{Member.table_name}.user_id = ? OR (#{Project.table_name}.is_public = ? AND #{Principal.table_name}.type = ?)", self.id, true, group_class.name).
572 where("#{Member.table_name}.user_id = ? OR (#{Project.table_name}.is_public = ? AND #{Principal.table_name}.type = ?)", self.id, true, group_class.name).
572 preload(:project, :roles).
573 preload(:project, :roles).
573 to_a
574 to_a
574
575
575 members.reject! {|member| member.user_id != id && project_ids.include?(member.project_id)}
576 members.reject! {|member| member.user_id != id && project_ids.include?(member.project_id)}
576 members.each do |member|
577 members.each do |member|
577 if member.project
578 if member.project
578 member.roles.each do |role|
579 member.roles.each do |role|
579 hash[role] = [] unless hash.key?(role)
580 hash[role] = [] unless hash.key?(role)
580 hash[role] << member.project
581 hash[role] << member.project
581 end
582 end
582 end
583 end
583 end
584 end
584
585
585 hash.each do |role, projects|
586 hash.each do |role, projects|
586 projects.uniq!
587 projects.uniq!
587 end
588 end
588
589
589 @projects_by_role = hash
590 @projects_by_role = hash
590 end
591 end
591
592
592 # Returns the ids of visible projects
593 # Returns the ids of visible projects
593 def visible_project_ids
594 def visible_project_ids
594 @visible_project_ids ||= Project.visible(self).pluck(:id)
595 @visible_project_ids ||= Project.visible(self).pluck(:id)
595 end
596 end
596
597
597 # Returns the roles that the user is allowed to manage for the given project
598 # Returns the roles that the user is allowed to manage for the given project
598 def managed_roles(project)
599 def managed_roles(project)
599 if admin?
600 if admin?
600 @managed_roles ||= Role.givable.to_a
601 @managed_roles ||= Role.givable.to_a
601 else
602 else
602 membership(project).try(:managed_roles) || []
603 membership(project).try(:managed_roles) || []
603 end
604 end
604 end
605 end
605
606
606 # Returns true if user is arg or belongs to arg
607 # Returns true if user is arg or belongs to arg
607 def is_or_belongs_to?(arg)
608 def is_or_belongs_to?(arg)
608 if arg.is_a?(User)
609 if arg.is_a?(User)
609 self == arg
610 self == arg
610 elsif arg.is_a?(Group)
611 elsif arg.is_a?(Group)
611 arg.users.include?(self)
612 arg.users.include?(self)
612 else
613 else
613 false
614 false
614 end
615 end
615 end
616 end
616
617
617 # Return true if the user is allowed to do the specified action on a specific context
618 # Return true if the user is allowed to do the specified action on a specific context
618 # Action can be:
619 # Action can be:
619 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
620 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
620 # * a permission Symbol (eg. :edit_project)
621 # * a permission Symbol (eg. :edit_project)
621 # Context can be:
622 # Context can be:
622 # * a project : returns true if user is allowed to do the specified action on this project
623 # * a project : returns true if user is allowed to do the specified action on this project
623 # * an array of projects : returns true if user is allowed on every project
624 # * an array of projects : returns true if user is allowed on every project
624 # * nil with options[:global] set : check if user has at least one role allowed for this action,
625 # * nil with options[:global] set : check if user has at least one role allowed for this action,
625 # or falls back to Non Member / Anonymous permissions depending if the user is logged
626 # or falls back to Non Member / Anonymous permissions depending if the user is logged
626 def allowed_to?(action, context, options={}, &block)
627 def allowed_to?(action, context, options={}, &block)
627 if context && context.is_a?(Project)
628 if context && context.is_a?(Project)
628 return false unless context.allows_to?(action)
629 return false unless context.allows_to?(action)
629 # Admin users are authorized for anything else
630 # Admin users are authorized for anything else
630 return true if admin?
631 return true if admin?
631
632
632 roles = roles_for_project(context)
633 roles = roles_for_project(context)
633 return false unless roles
634 return false unless roles
634 roles.any? {|role|
635 roles.any? {|role|
635 (context.is_public? || role.member?) &&
636 (context.is_public? || role.member?) &&
636 role.allowed_to?(action) &&
637 role.allowed_to?(action) &&
637 (block_given? ? yield(role, self) : true)
638 (block_given? ? yield(role, self) : true)
638 }
639 }
639 elsif context && context.is_a?(Array)
640 elsif context && context.is_a?(Array)
640 if context.empty?
641 if context.empty?
641 false
642 false
642 else
643 else
643 # Authorize if user is authorized on every element of the array
644 # Authorize if user is authorized on every element of the array
644 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
645 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
645 end
646 end
646 elsif context
647 elsif context
647 raise ArgumentError.new("#allowed_to? context argument must be a Project, an Array of projects or nil")
648 raise ArgumentError.new("#allowed_to? context argument must be a Project, an Array of projects or nil")
648 elsif options[:global]
649 elsif options[:global]
649 # Admin users are always authorized
650 # Admin users are always authorized
650 return true if admin?
651 return true if admin?
651
652
652 # authorize if user has at least one role that has this permission
653 # authorize if user has at least one role that has this permission
653 roles = memberships.collect {|m| m.roles}.flatten.uniq
654 roles = memberships.collect {|m| m.roles}.flatten.uniq
654 roles << (self.logged? ? Role.non_member : Role.anonymous)
655 roles << (self.logged? ? Role.non_member : Role.anonymous)
655 roles.any? {|role|
656 roles.any? {|role|
656 role.allowed_to?(action) &&
657 role.allowed_to?(action) &&
657 (block_given? ? yield(role, self) : true)
658 (block_given? ? yield(role, self) : true)
658 }
659 }
659 else
660 else
660 false
661 false
661 end
662 end
662 end
663 end
663
664
664 # Is the user allowed to do the specified action on any project?
665 # Is the user allowed to do the specified action on any project?
665 # See allowed_to? for the actions and valid options.
666 # See allowed_to? for the actions and valid options.
666 #
667 #
667 # NB: this method is not used anywhere in the core codebase as of
668 # NB: this method is not used anywhere in the core codebase as of
668 # 2.5.2, but it's used by many plugins so if we ever want to remove
669 # 2.5.2, but it's used by many plugins so if we ever want to remove
669 # it it has to be carefully deprecated for a version or two.
670 # it it has to be carefully deprecated for a version or two.
670 def allowed_to_globally?(action, options={}, &block)
671 def allowed_to_globally?(action, options={}, &block)
671 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
672 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
672 end
673 end
673
674
674 def allowed_to_view_all_time_entries?(context)
675 def allowed_to_view_all_time_entries?(context)
675 allowed_to?(:view_time_entries, context) do |role, user|
676 allowed_to?(:view_time_entries, context) do |role, user|
676 role.time_entries_visibility == 'all'
677 role.time_entries_visibility == 'all'
677 end
678 end
678 end
679 end
679
680
680 # Returns true if the user is allowed to delete the user's own account
681 # Returns true if the user is allowed to delete the user's own account
681 def own_account_deletable?
682 def own_account_deletable?
682 Setting.unsubscribe? &&
683 Setting.unsubscribe? &&
683 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
684 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
684 end
685 end
685
686
686 safe_attributes 'firstname',
687 safe_attributes 'firstname',
687 'lastname',
688 'lastname',
688 'mail',
689 'mail',
689 'mail_notification',
690 'mail_notification',
690 'notified_project_ids',
691 'notified_project_ids',
691 'language',
692 'language',
692 'custom_field_values',
693 'custom_field_values',
693 'custom_fields',
694 'custom_fields',
694 'identity_url'
695 'identity_url'
695
696
696 safe_attributes 'status',
697 safe_attributes 'status',
697 'auth_source_id',
698 'auth_source_id',
698 'generate_password',
699 'generate_password',
699 'must_change_passwd',
700 'must_change_passwd',
700 :if => lambda {|user, current_user| current_user.admin?}
701 :if => lambda {|user, current_user| current_user.admin?}
701
702
702 safe_attributes 'group_ids',
703 safe_attributes 'group_ids',
703 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
704 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
704
705
705 # Utility method to help check if a user should be notified about an
706 # Utility method to help check if a user should be notified about an
706 # event.
707 # event.
707 #
708 #
708 # TODO: only supports Issue events currently
709 # TODO: only supports Issue events currently
709 def notify_about?(object)
710 def notify_about?(object)
710 if mail_notification == 'all'
711 if mail_notification == 'all'
711 true
712 true
712 elsif mail_notification.blank? || mail_notification == 'none'
713 elsif mail_notification.blank? || mail_notification == 'none'
713 false
714 false
714 else
715 else
715 case object
716 case object
716 when Issue
717 when Issue
717 case mail_notification
718 case mail_notification
718 when 'selected', 'only_my_events'
719 when 'selected', 'only_my_events'
719 # user receives notifications for created/assigned issues on unselected projects
720 # user receives notifications for created/assigned issues on unselected projects
720 object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
721 object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
721 when 'only_assigned'
722 when 'only_assigned'
722 is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
723 is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
723 when 'only_owner'
724 when 'only_owner'
724 object.author == self
725 object.author == self
725 end
726 end
726 when News
727 when News
727 # always send to project members except when mail_notification is set to 'none'
728 # always send to project members except when mail_notification is set to 'none'
728 true
729 true
729 end
730 end
730 end
731 end
731 end
732 end
732
733
733 def self.current=(user)
734 def self.current=(user)
734 RequestStore.store[:current_user] = user
735 RequestStore.store[:current_user] = user
735 end
736 end
736
737
737 def self.current
738 def self.current
738 RequestStore.store[:current_user] ||= User.anonymous
739 RequestStore.store[:current_user] ||= User.anonymous
739 end
740 end
740
741
741 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
742 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
742 # one anonymous user per database.
743 # one anonymous user per database.
743 def self.anonymous
744 def self.anonymous
744 anonymous_user = AnonymousUser.first
745 anonymous_user = AnonymousUser.first
745 if anonymous_user.nil?
746 if anonymous_user.nil?
746 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :login => '', :status => 0)
747 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :login => '', :status => 0)
747 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
748 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
748 end
749 end
749 anonymous_user
750 anonymous_user
750 end
751 end
751
752
752 # Salts all existing unsalted passwords
753 # Salts all existing unsalted passwords
753 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
754 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
754 # This method is used in the SaltPasswords migration and is to be kept as is
755 # This method is used in the SaltPasswords migration and is to be kept as is
755 def self.salt_unsalted_passwords!
756 def self.salt_unsalted_passwords!
756 transaction do
757 transaction do
757 User.where("salt IS NULL OR salt = ''").find_each do |user|
758 User.where("salt IS NULL OR salt = ''").find_each do |user|
758 next if user.hashed_password.blank?
759 next if user.hashed_password.blank?
759 salt = User.generate_salt
760 salt = User.generate_salt
760 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
761 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
761 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
762 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
762 end
763 end
763 end
764 end
764 end
765 end
765
766
766 protected
767 protected
767
768
768 def validate_password_length
769 def validate_password_length
769 return if password.blank? && generate_password?
770 return if password.blank? && generate_password?
770 # Password length validation based on setting
771 # Password length validation based on setting
771 if !password.nil? && password.size < Setting.password_min_length.to_i
772 if !password.nil? && password.size < Setting.password_min_length.to_i
772 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
773 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
773 end
774 end
774 end
775 end
775
776
776 def instantiate_email_address
777 def instantiate_email_address
777 email_address || build_email_address
778 email_address || build_email_address
778 end
779 end
779
780
780 private
781 private
781
782
782 def generate_password_if_needed
783 def generate_password_if_needed
783 if generate_password? && auth_source.nil?
784 if generate_password? && auth_source.nil?
784 length = [Setting.password_min_length.to_i + 2, 10].max
785 length = [Setting.password_min_length.to_i + 2, 10].max
785 random_password(length)
786 random_password(length)
786 end
787 end
787 end
788 end
788
789
789 # Delete all outstanding password reset tokens on password change.
790 # Delete all outstanding password reset tokens on password change.
790 # Delete the autologin tokens on password change to prohibit session leakage.
791 # Delete the autologin tokens on password change to prohibit session leakage.
791 # This helps to keep the account secure in case the associated email account
792 # This helps to keep the account secure in case the associated email account
792 # was compromised.
793 # was compromised.
793 def destroy_tokens
794 def destroy_tokens
794 if hashed_password_changed? || (status_changed? && !active?)
795 if hashed_password_changed? || (status_changed? && !active?)
795 tokens = ['recovery', 'autologin', 'session']
796 tokens = ['recovery', 'autologin', 'session']
796 Token.where(:user_id => id, :action => tokens).delete_all
797 Token.where(:user_id => id, :action => tokens).delete_all
797 end
798 end
798 end
799 end
799
800
800 # Removes references that are not handled by associations
801 # Removes references that are not handled by associations
801 # Things that are not deleted are reassociated with the anonymous user
802 # Things that are not deleted are reassociated with the anonymous user
802 def remove_references_before_destroy
803 def remove_references_before_destroy
803 return if self.id.nil?
804 return if self.id.nil?
804
805
805 substitute = User.anonymous
806 substitute = User.anonymous
806 Attachment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
807 Attachment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
807 Comment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
808 Comment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
808 Issue.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
809 Issue.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
809 Issue.where(['assigned_to_id = ?', id]).update_all('assigned_to_id = NULL')
810 Issue.where(['assigned_to_id = ?', id]).update_all('assigned_to_id = NULL')
810 Journal.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
811 Journal.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
811 JournalDetail.
812 JournalDetail.
812 where(["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]).
813 where(["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]).
813 update_all(['old_value = ?', substitute.id.to_s])
814 update_all(['old_value = ?', substitute.id.to_s])
814 JournalDetail.
815 JournalDetail.
815 where(["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]).
816 where(["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]).
816 update_all(['value = ?', substitute.id.to_s])
817 update_all(['value = ?', substitute.id.to_s])
817 Message.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
818 Message.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
818 News.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
819 News.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
819 # Remove private queries and keep public ones
820 # Remove private queries and keep public ones
820 ::Query.delete_all ['user_id = ? AND visibility = ?', id, ::Query::VISIBILITY_PRIVATE]
821 ::Query.delete_all ['user_id = ? AND visibility = ?', id, ::Query::VISIBILITY_PRIVATE]
821 ::Query.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
822 ::Query.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
822 TimeEntry.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
823 TimeEntry.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
823 Token.delete_all ['user_id = ?', id]
824 Token.delete_all ['user_id = ?', id]
824 Watcher.delete_all ['user_id = ?', id]
825 Watcher.delete_all ['user_id = ?', id]
825 WikiContent.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
826 WikiContent.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
826 WikiContent::Version.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
827 WikiContent::Version.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
827 end
828 end
828
829
829 # Return password digest
830 # Return password digest
830 def self.hash_password(clear_password)
831 def self.hash_password(clear_password)
831 Digest::SHA1.hexdigest(clear_password || "")
832 Digest::SHA1.hexdigest(clear_password || "")
832 end
833 end
833
834
834 # Returns a 128bits random salt as a hex string (32 chars long)
835 # Returns a 128bits random salt as a hex string (32 chars long)
835 def self.generate_salt
836 def self.generate_salt
836 Redmine::Utils.random_hex(16)
837 Redmine::Utils.random_hex(16)
837 end
838 end
839 # Send a security notification to all admins if the user has gained/lost admin privileges
840 def deliver_security_notification
841 options = {
842 field: :field_admin,
843 value: login,
844 title: :label_user_plural,
845 url: {controller: 'users', action: 'index'}
846 }
847 deliver = false
848 if (admin? && id_changed? && active?) || # newly created admin
849 (admin? && admin_changed? && active?) || # regular user became admin
850 (admin? && status_changed? && active?) # locked admin became active again
851
852 deliver = true
853 options[:message] = :mail_body_security_notification_add
854
855 elsif (admin? && destroyed? && active?) || # active admin user was deleted
856 (!admin? && admin_changed? && active?) || # admin is no longer admin
857 (admin? && status_changed? && !active?) # admin was locked
858
859 deliver = true
860 options[:message] = :mail_body_security_notification_remove
861 end
862
863 User.where(admin: true, status: Principal::STATUS_ACTIVE).each{|u| Mailer.security_notification(u, options).deliver} if deliver
864 end
865
866
838
867
839 end
868 end
840
869
841 class AnonymousUser < User
870 class AnonymousUser < User
842 validate :validate_anonymous_uniqueness, :on => :create
871 validate :validate_anonymous_uniqueness, :on => :create
843
872
844 def validate_anonymous_uniqueness
873 def validate_anonymous_uniqueness
845 # There should be only one AnonymousUser in the database
874 # There should be only one AnonymousUser in the database
846 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
875 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
847 end
876 end
848
877
849 def available_custom_fields
878 def available_custom_fields
850 []
879 []
851 end
880 end
852
881
853 # Overrides a few properties
882 # Overrides a few properties
854 def logged?; false end
883 def logged?; false end
855 def admin; false end
884 def admin; false end
856 def name(*args); I18n.t(:label_user_anonymous) end
885 def name(*args); I18n.t(:label_user_anonymous) end
857 def mail=(*args); nil end
886 def mail=(*args); nil end
858 def mail; nil end
887 def mail; nil end
859 def time_zone; nil end
888 def time_zone; nil end
860 def rss_key; nil end
889 def rss_key; nil end
861
890
862 def pref
891 def pref
863 UserPreference.new(:user => self)
892 UserPreference.new(:user => self)
864 end
893 end
865
894
866 # Returns the user's bult-in role
895 # Returns the user's bult-in role
867 def builtin_role
896 def builtin_role
868 @builtin_role ||= Role.anonymous
897 @builtin_role ||= Role.anonymous
869 end
898 end
870
899
871 def membership(*args)
900 def membership(*args)
872 nil
901 nil
873 end
902 end
874
903
875 def member_of?(*args)
904 def member_of?(*args)
876 false
905 false
877 end
906 end
878
907
879 # Anonymous user can not be destroyed
908 # Anonymous user can not be destroyed
880 def destroy
909 def destroy
881 false
910 false
882 end
911 end
883
912
884 protected
913 protected
885
914
886 def instantiate_email_address
915 def instantiate_email_address
887 end
916 end
888 end
917 end
@@ -1,452 +1,596
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class UsersControllerTest < ActionController::TestCase
20 class UsersControllerTest < ActionController::TestCase
21 include Redmine::I18n
21 include Redmine::I18n
22
22
23 fixtures :users, :email_addresses, :projects, :members, :member_roles, :roles,
23 fixtures :users, :email_addresses, :projects, :members, :member_roles, :roles,
24 :custom_fields, :custom_values, :groups_users,
24 :custom_fields, :custom_values, :groups_users,
25 :auth_sources,
25 :auth_sources,
26 :enabled_modules,
26 :enabled_modules,
27 :issues, :issue_statuses,
27 :issues, :issue_statuses,
28 :trackers
28 :trackers
29
29
30 def setup
30 def setup
31 User.current = nil
31 User.current = nil
32 @request.session[:user_id] = 1 # admin
32 @request.session[:user_id] = 1 # admin
33 end
33 end
34
34
35 def test_index
35 def test_index
36 get :index
36 get :index
37 assert_response :success
37 assert_response :success
38 assert_template 'index'
38 assert_template 'index'
39 assert_not_nil assigns(:users)
39 assert_not_nil assigns(:users)
40 # active users only
40 # active users only
41 assert_nil assigns(:users).detect {|u| !u.active?}
41 assert_nil assigns(:users).detect {|u| !u.active?}
42 end
42 end
43
43
44 def test_index_with_status_filter
44 def test_index_with_status_filter
45 get :index, :status => 3
45 get :index, :status => 3
46 assert_response :success
46 assert_response :success
47 assert_template 'index'
47 assert_template 'index'
48 assert_not_nil assigns(:users)
48 assert_not_nil assigns(:users)
49 assert_equal [3], assigns(:users).map(&:status).uniq
49 assert_equal [3], assigns(:users).map(&:status).uniq
50 end
50 end
51
51
52 def test_index_with_name_filter
52 def test_index_with_name_filter
53 get :index, :name => 'john'
53 get :index, :name => 'john'
54 assert_response :success
54 assert_response :success
55 assert_template 'index'
55 assert_template 'index'
56 users = assigns(:users)
56 users = assigns(:users)
57 assert_not_nil users
57 assert_not_nil users
58 assert_equal 1, users.size
58 assert_equal 1, users.size
59 assert_equal 'John', users.first.firstname
59 assert_equal 'John', users.first.firstname
60 end
60 end
61
61
62 def test_index_with_group_filter
62 def test_index_with_group_filter
63 get :index, :group_id => '10'
63 get :index, :group_id => '10'
64 assert_response :success
64 assert_response :success
65 assert_template 'index'
65 assert_template 'index'
66 users = assigns(:users)
66 users = assigns(:users)
67 assert users.any?
67 assert users.any?
68 assert_equal([], (users - Group.find(10).users))
68 assert_equal([], (users - Group.find(10).users))
69 assert_select 'select[name=group_id]' do
69 assert_select 'select[name=group_id]' do
70 assert_select 'option[value="10"][selected=selected]'
70 assert_select 'option[value="10"][selected=selected]'
71 end
71 end
72 end
72 end
73
73
74 def test_show
74 def test_show
75 @request.session[:user_id] = nil
75 @request.session[:user_id] = nil
76 get :show, :id => 2
76 get :show, :id => 2
77 assert_response :success
77 assert_response :success
78 assert_template 'show'
78 assert_template 'show'
79 assert_not_nil assigns(:user)
79 assert_not_nil assigns(:user)
80
80
81 assert_select 'li', :text => /Phone number/
81 assert_select 'li', :text => /Phone number/
82 end
82 end
83
83
84 def test_show_should_not_display_hidden_custom_fields
84 def test_show_should_not_display_hidden_custom_fields
85 @request.session[:user_id] = nil
85 @request.session[:user_id] = nil
86 UserCustomField.find_by_name('Phone number').update_attribute :visible, false
86 UserCustomField.find_by_name('Phone number').update_attribute :visible, false
87 get :show, :id => 2
87 get :show, :id => 2
88 assert_response :success
88 assert_response :success
89 assert_template 'show'
89 assert_template 'show'
90 assert_not_nil assigns(:user)
90 assert_not_nil assigns(:user)
91
91
92 assert_select 'li', :text => /Phone number/, :count => 0
92 assert_select 'li', :text => /Phone number/, :count => 0
93 end
93 end
94
94
95 def test_show_should_not_fail_when_custom_values_are_nil
95 def test_show_should_not_fail_when_custom_values_are_nil
96 user = User.find(2)
96 user = User.find(2)
97
97
98 # Create a custom field to illustrate the issue
98 # Create a custom field to illustrate the issue
99 custom_field = CustomField.create!(:name => 'Testing', :field_format => 'text')
99 custom_field = CustomField.create!(:name => 'Testing', :field_format => 'text')
100 custom_value = user.custom_values.build(:custom_field => custom_field).save!
100 custom_value = user.custom_values.build(:custom_field => custom_field).save!
101
101
102 get :show, :id => 2
102 get :show, :id => 2
103 assert_response :success
103 assert_response :success
104 end
104 end
105
105
106 def test_show_inactive
106 def test_show_inactive
107 @request.session[:user_id] = nil
107 @request.session[:user_id] = nil
108 get :show, :id => 5
108 get :show, :id => 5
109 assert_response 404
109 assert_response 404
110 end
110 end
111
111
112 def test_show_inactive_by_admin
112 def test_show_inactive_by_admin
113 @request.session[:user_id] = 1
113 @request.session[:user_id] = 1
114 get :show, :id => 5
114 get :show, :id => 5
115 assert_response 200
115 assert_response 200
116 assert_not_nil assigns(:user)
116 assert_not_nil assigns(:user)
117 end
117 end
118
118
119 def test_show_user_who_is_not_visible_should_return_404
119 def test_show_user_who_is_not_visible_should_return_404
120 Role.anonymous.update! :users_visibility => 'members_of_visible_projects'
120 Role.anonymous.update! :users_visibility => 'members_of_visible_projects'
121 user = User.generate!
121 user = User.generate!
122
122
123 @request.session[:user_id] = nil
123 @request.session[:user_id] = nil
124 get :show, :id => user.id
124 get :show, :id => user.id
125 assert_response 404
125 assert_response 404
126 end
126 end
127
127
128 def test_show_displays_memberships_based_on_project_visibility
128 def test_show_displays_memberships_based_on_project_visibility
129 @request.session[:user_id] = 1
129 @request.session[:user_id] = 1
130 get :show, :id => 2
130 get :show, :id => 2
131 assert_response :success
131 assert_response :success
132 memberships = assigns(:memberships)
132 memberships = assigns(:memberships)
133 assert_not_nil memberships
133 assert_not_nil memberships
134 project_ids = memberships.map(&:project_id)
134 project_ids = memberships.map(&:project_id)
135 assert project_ids.include?(2) #private project admin can see
135 assert project_ids.include?(2) #private project admin can see
136 end
136 end
137
137
138 def test_show_current_should_require_authentication
138 def test_show_current_should_require_authentication
139 @request.session[:user_id] = nil
139 @request.session[:user_id] = nil
140 get :show, :id => 'current'
140 get :show, :id => 'current'
141 assert_response 302
141 assert_response 302
142 end
142 end
143
143
144 def test_show_current
144 def test_show_current
145 @request.session[:user_id] = 2
145 @request.session[:user_id] = 2
146 get :show, :id => 'current'
146 get :show, :id => 'current'
147 assert_response :success
147 assert_response :success
148 assert_template 'show'
148 assert_template 'show'
149 assert_equal User.find(2), assigns(:user)
149 assert_equal User.find(2), assigns(:user)
150 end
150 end
151
151
152 def test_new
152 def test_new
153 get :new
153 get :new
154 assert_response :success
154 assert_response :success
155 assert_template :new
155 assert_template :new
156 assert assigns(:user)
156 assert assigns(:user)
157 end
157 end
158
158
159 def test_create
159 def test_create
160 Setting.bcc_recipients = '1'
160 Setting.bcc_recipients = '1'
161
161
162 assert_difference 'User.count' do
162 assert_difference 'User.count' do
163 assert_difference 'ActionMailer::Base.deliveries.size' do
163 assert_difference 'ActionMailer::Base.deliveries.size' do
164 post :create,
164 post :create,
165 :user => {
165 :user => {
166 :firstname => 'John',
166 :firstname => 'John',
167 :lastname => 'Doe',
167 :lastname => 'Doe',
168 :login => 'jdoe',
168 :login => 'jdoe',
169 :password => 'secret123',
169 :password => 'secret123',
170 :password_confirmation => 'secret123',
170 :password_confirmation => 'secret123',
171 :mail => 'jdoe@gmail.com',
171 :mail => 'jdoe@gmail.com',
172 :mail_notification => 'none'
172 :mail_notification => 'none'
173 },
173 },
174 :send_information => '1'
174 :send_information => '1'
175 end
175 end
176 end
176 end
177
177
178 user = User.order('id DESC').first
178 user = User.order('id DESC').first
179 assert_redirected_to :controller => 'users', :action => 'edit', :id => user.id
179 assert_redirected_to :controller => 'users', :action => 'edit', :id => user.id
180
180
181 assert_equal 'John', user.firstname
181 assert_equal 'John', user.firstname
182 assert_equal 'Doe', user.lastname
182 assert_equal 'Doe', user.lastname
183 assert_equal 'jdoe', user.login
183 assert_equal 'jdoe', user.login
184 assert_equal 'jdoe@gmail.com', user.mail
184 assert_equal 'jdoe@gmail.com', user.mail
185 assert_equal 'none', user.mail_notification
185 assert_equal 'none', user.mail_notification
186 assert user.check_password?('secret123')
186 assert user.check_password?('secret123')
187
187
188 mail = ActionMailer::Base.deliveries.last
188 mail = ActionMailer::Base.deliveries.last
189 assert_not_nil mail
189 assert_not_nil mail
190 assert_equal [user.mail], mail.bcc
190 assert_equal [user.mail], mail.bcc
191 assert_mail_body_match 'secret', mail
191 assert_mail_body_match 'secret', mail
192 end
192 end
193
193
194 def test_create_with_preferences
194 def test_create_with_preferences
195 assert_difference 'User.count' do
195 assert_difference 'User.count' do
196 post :create,
196 post :create,
197 :user => {
197 :user => {
198 :firstname => 'John',
198 :firstname => 'John',
199 :lastname => 'Doe',
199 :lastname => 'Doe',
200 :login => 'jdoe',
200 :login => 'jdoe',
201 :password => 'secret123',
201 :password => 'secret123',
202 :password_confirmation => 'secret123',
202 :password_confirmation => 'secret123',
203 :mail => 'jdoe@gmail.com',
203 :mail => 'jdoe@gmail.com',
204 :mail_notification => 'none'
204 :mail_notification => 'none'
205 },
205 },
206 :pref => {
206 :pref => {
207 'hide_mail' => '1',
207 'hide_mail' => '1',
208 'time_zone' => 'Paris',
208 'time_zone' => 'Paris',
209 'comments_sorting' => 'desc',
209 'comments_sorting' => 'desc',
210 'warn_on_leaving_unsaved' => '0'
210 'warn_on_leaving_unsaved' => '0'
211 }
211 }
212 end
212 end
213 user = User.order('id DESC').first
213 user = User.order('id DESC').first
214 assert_equal 'jdoe', user.login
214 assert_equal 'jdoe', user.login
215 assert_equal true, user.pref.hide_mail
215 assert_equal true, user.pref.hide_mail
216 assert_equal 'Paris', user.pref.time_zone
216 assert_equal 'Paris', user.pref.time_zone
217 assert_equal 'desc', user.pref[:comments_sorting]
217 assert_equal 'desc', user.pref[:comments_sorting]
218 assert_equal '0', user.pref[:warn_on_leaving_unsaved]
218 assert_equal '0', user.pref[:warn_on_leaving_unsaved]
219 end
219 end
220
220
221 def test_create_with_generate_password_should_email_the_password
221 def test_create_with_generate_password_should_email_the_password
222 assert_difference 'User.count' do
222 assert_difference 'User.count' do
223 post :create, :user => {
223 post :create, :user => {
224 :login => 'randompass',
224 :login => 'randompass',
225 :firstname => 'Random',
225 :firstname => 'Random',
226 :lastname => 'Pass',
226 :lastname => 'Pass',
227 :mail => 'randompass@example.net',
227 :mail => 'randompass@example.net',
228 :language => 'en',
228 :language => 'en',
229 :generate_password => '1',
229 :generate_password => '1',
230 :password => '',
230 :password => '',
231 :password_confirmation => ''
231 :password_confirmation => ''
232 }, :send_information => 1
232 }, :send_information => 1
233 end
233 end
234 user = User.order('id DESC').first
234 user = User.order('id DESC').first
235 assert_equal 'randompass', user.login
235 assert_equal 'randompass', user.login
236
236
237 mail = ActionMailer::Base.deliveries.last
237 mail = ActionMailer::Base.deliveries.last
238 assert_not_nil mail
238 assert_not_nil mail
239 m = mail_body(mail).match(/Password: ([a-zA-Z0-9]+)/)
239 m = mail_body(mail).match(/Password: ([a-zA-Z0-9]+)/)
240 assert m
240 assert m
241 password = m[1]
241 password = m[1]
242 assert user.check_password?(password)
242 assert user.check_password?(password)
243 end
243 end
244
244
245 def test_create_and_continue
245 def test_create_and_continue
246 post :create, :user => {
246 post :create, :user => {
247 :login => 'randompass',
247 :login => 'randompass',
248 :firstname => 'Random',
248 :firstname => 'Random',
249 :lastname => 'Pass',
249 :lastname => 'Pass',
250 :mail => 'randompass@example.net',
250 :mail => 'randompass@example.net',
251 :generate_password => '1'
251 :generate_password => '1'
252 }, :continue => '1'
252 }, :continue => '1'
253 assert_redirected_to '/users/new?user%5Bgenerate_password%5D=1'
253 assert_redirected_to '/users/new?user%5Bgenerate_password%5D=1'
254 end
254 end
255
255
256 def test_create_with_failure
256 def test_create_with_failure
257 assert_no_difference 'User.count' do
257 assert_no_difference 'User.count' do
258 post :create, :user => {}
258 post :create, :user => {}
259 end
259 end
260 assert_response :success
260 assert_response :success
261 assert_template 'new'
261 assert_template 'new'
262 end
262 end
263
263
264 def test_create_with_failure_sould_preserve_preference
264 def test_create_with_failure_sould_preserve_preference
265 assert_no_difference 'User.count' do
265 assert_no_difference 'User.count' do
266 post :create,
266 post :create,
267 :user => {},
267 :user => {},
268 :pref => {
268 :pref => {
269 'no_self_notified' => '1',
269 'no_self_notified' => '1',
270 'hide_mail' => '1',
270 'hide_mail' => '1',
271 'time_zone' => 'Paris',
271 'time_zone' => 'Paris',
272 'comments_sorting' => 'desc',
272 'comments_sorting' => 'desc',
273 'warn_on_leaving_unsaved' => '0'
273 'warn_on_leaving_unsaved' => '0'
274 }
274 }
275 end
275 end
276 assert_response :success
276 assert_response :success
277 assert_template 'new'
277 assert_template 'new'
278
278
279 assert_select 'select#pref_time_zone option[selected=selected]', :text => /Paris/
279 assert_select 'select#pref_time_zone option[selected=selected]', :text => /Paris/
280 assert_select 'input#pref_no_self_notified[value="1"][checked=checked]'
280 assert_select 'input#pref_no_self_notified[value="1"][checked=checked]'
281 end
281 end
282
282
283 def test_create_admin_should_send_security_notification
284 ActionMailer::Base.deliveries.clear
285 post :create,
286 :user => {
287 :firstname => 'Edgar',
288 :lastname => 'Schmoe',
289 :login => 'eschmoe',
290 :password => 'secret123',
291 :password_confirmation => 'secret123',
292 :mail => 'eschmoe@example.foo',
293 :admin => '1'
294 }
295
296 assert_not_nil (mail = ActionMailer::Base.deliveries.last)
297 assert_mail_body_match '0.0.0.0', mail
298 assert_mail_body_match I18n.t(:mail_body_security_notification_add, field: I18n.t(:field_admin), value: 'eschmoe'), mail
299 assert_select_email do
300 assert_select 'a[href^=?]', 'http://localhost:3000/users', :text => 'Users'
301 end
302
303 # All admins should receive this
304 User.where(admin: true, status: Principal::STATUS_ACTIVE).each do |admin|
305 assert_not_nil ActionMailer::Base.deliveries.detect{|mail| [mail.bcc, mail.cc].flatten.include?(admin.mail) }
306 end
307 end
308
309 def test_create_non_admin_should_not_send_security_notification
310 ActionMailer::Base.deliveries.clear
311 post :create,
312 :user => {
313 :firstname => 'Edgar',
314 :lastname => 'Schmoe',
315 :login => 'eschmoe',
316 :password => 'secret123',
317 :password_confirmation => 'secret123',
318 :mail => 'eschmoe@example.foo',
319 :admin => '0'
320 }
321 assert_nil ActionMailer::Base.deliveries.last
322 end
323
324
283 def test_edit
325 def test_edit
284 get :edit, :id => 2
326 get :edit, :id => 2
285 assert_response :success
327 assert_response :success
286 assert_template 'edit'
328 assert_template 'edit'
287 assert_equal User.find(2), assigns(:user)
329 assert_equal User.find(2), assigns(:user)
288 end
330 end
289
331
290 def test_edit_registered_user
332 def test_edit_registered_user
291 assert User.find(2).register!
333 assert User.find(2).register!
292
334
293 get :edit, :id => 2
335 get :edit, :id => 2
294 assert_response :success
336 assert_response :success
295 assert_select 'a', :text => 'Activate'
337 assert_select 'a', :text => 'Activate'
296 end
338 end
297
339
298 def test_update
340 def test_update
299 ActionMailer::Base.deliveries.clear
341 ActionMailer::Base.deliveries.clear
300 put :update, :id => 2,
342 put :update, :id => 2,
301 :user => {:firstname => 'Changed', :mail_notification => 'only_assigned'},
343 :user => {:firstname => 'Changed', :mail_notification => 'only_assigned'},
302 :pref => {:hide_mail => '1', :comments_sorting => 'desc'}
344 :pref => {:hide_mail => '1', :comments_sorting => 'desc'}
303 user = User.find(2)
345 user = User.find(2)
304 assert_equal 'Changed', user.firstname
346 assert_equal 'Changed', user.firstname
305 assert_equal 'only_assigned', user.mail_notification
347 assert_equal 'only_assigned', user.mail_notification
306 assert_equal true, user.pref[:hide_mail]
348 assert_equal true, user.pref[:hide_mail]
307 assert_equal 'desc', user.pref[:comments_sorting]
349 assert_equal 'desc', user.pref[:comments_sorting]
308 assert ActionMailer::Base.deliveries.empty?
350 assert ActionMailer::Base.deliveries.empty?
309 end
351 end
310
352
311 def test_update_with_failure
353 def test_update_with_failure
312 assert_no_difference 'User.count' do
354 assert_no_difference 'User.count' do
313 put :update, :id => 2, :user => {:firstname => ''}
355 put :update, :id => 2, :user => {:firstname => ''}
314 end
356 end
315 assert_response :success
357 assert_response :success
316 assert_template 'edit'
358 assert_template 'edit'
317 end
359 end
318
360
319 def test_update_with_group_ids_should_assign_groups
361 def test_update_with_group_ids_should_assign_groups
320 put :update, :id => 2, :user => {:group_ids => ['10']}
362 put :update, :id => 2, :user => {:group_ids => ['10']}
321 user = User.find(2)
363 user = User.find(2)
322 assert_equal [10], user.group_ids
364 assert_equal [10], user.group_ids
323 end
365 end
324
366
325 def test_update_with_activation_should_send_a_notification
367 def test_update_with_activation_should_send_a_notification
326 u = User.new(:firstname => 'Foo', :lastname => 'Bar', :mail => 'foo.bar@somenet.foo', :language => 'fr')
368 u = User.new(:firstname => 'Foo', :lastname => 'Bar', :mail => 'foo.bar@somenet.foo', :language => 'fr')
327 u.login = 'foo'
369 u.login = 'foo'
328 u.status = User::STATUS_REGISTERED
370 u.status = User::STATUS_REGISTERED
329 u.save!
371 u.save!
330 ActionMailer::Base.deliveries.clear
372 ActionMailer::Base.deliveries.clear
331 Setting.bcc_recipients = '1'
373 Setting.bcc_recipients = '1'
332
374
333 put :update, :id => u.id, :user => {:status => User::STATUS_ACTIVE}
375 put :update, :id => u.id, :user => {:status => User::STATUS_ACTIVE}
334 assert u.reload.active?
376 assert u.reload.active?
335 mail = ActionMailer::Base.deliveries.last
377 mail = ActionMailer::Base.deliveries.last
336 assert_not_nil mail
378 assert_not_nil mail
337 assert_equal ['foo.bar@somenet.foo'], mail.bcc
379 assert_equal ['foo.bar@somenet.foo'], mail.bcc
338 assert_mail_body_match ll('fr', :notice_account_activated), mail
380 assert_mail_body_match ll('fr', :notice_account_activated), mail
339 end
381 end
340
382
341 def test_update_with_password_change_should_send_a_notification
383 def test_update_with_password_change_should_send_a_notification
342 ActionMailer::Base.deliveries.clear
384 ActionMailer::Base.deliveries.clear
343 Setting.bcc_recipients = '1'
385 Setting.bcc_recipients = '1'
344
386
345 put :update, :id => 2, :user => {:password => 'newpass123', :password_confirmation => 'newpass123'}, :send_information => '1'
387 put :update, :id => 2, :user => {:password => 'newpass123', :password_confirmation => 'newpass123'}, :send_information => '1'
346 u = User.find(2)
388 u = User.find(2)
347 assert u.check_password?('newpass123')
389 assert u.check_password?('newpass123')
348
390
349 mail = ActionMailer::Base.deliveries.last
391 mail = ActionMailer::Base.deliveries.last
350 assert_not_nil mail
392 assert_not_nil mail
351 assert_equal [u.mail], mail.bcc
393 assert_equal [u.mail], mail.bcc
352 assert_mail_body_match 'newpass123', mail
394 assert_mail_body_match 'newpass123', mail
353 end
395 end
354
396
355 def test_update_with_generate_password_should_email_the_password
397 def test_update_with_generate_password_should_email_the_password
356 ActionMailer::Base.deliveries.clear
398 ActionMailer::Base.deliveries.clear
357 Setting.bcc_recipients = '1'
399 Setting.bcc_recipients = '1'
358
400
359 put :update, :id => 2, :user => {
401 put :update, :id => 2, :user => {
360 :generate_password => '1',
402 :generate_password => '1',
361 :password => '',
403 :password => '',
362 :password_confirmation => ''
404 :password_confirmation => ''
363 }, :send_information => '1'
405 }, :send_information => '1'
364
406
365 mail = ActionMailer::Base.deliveries.last
407 mail = ActionMailer::Base.deliveries.last
366 assert_not_nil mail
408 assert_not_nil mail
367 m = mail_body(mail).match(/Password: ([a-zA-Z0-9]+)/)
409 m = mail_body(mail).match(/Password: ([a-zA-Z0-9]+)/)
368 assert m
410 assert m
369 password = m[1]
411 password = m[1]
370 assert User.find(2).check_password?(password)
412 assert User.find(2).check_password?(password)
371 end
413 end
372
414
373 def test_update_without_generate_password_should_not_change_password
415 def test_update_without_generate_password_should_not_change_password
374 put :update, :id => 2, :user => {
416 put :update, :id => 2, :user => {
375 :firstname => 'changed',
417 :firstname => 'changed',
376 :generate_password => '0',
418 :generate_password => '0',
377 :password => '',
419 :password => '',
378 :password_confirmation => ''
420 :password_confirmation => ''
379 }, :send_information => '1'
421 }, :send_information => '1'
380
422
381 user = User.find(2)
423 user = User.find(2)
382 assert_equal 'changed', user.firstname
424 assert_equal 'changed', user.firstname
383 assert user.check_password?('jsmith')
425 assert user.check_password?('jsmith')
384 end
426 end
385
427
386 def test_update_user_switchin_from_auth_source_to_password_authentication
428 def test_update_user_switchin_from_auth_source_to_password_authentication
387 # Configure as auth source
429 # Configure as auth source
388 u = User.find(2)
430 u = User.find(2)
389 u.auth_source = AuthSource.find(1)
431 u.auth_source = AuthSource.find(1)
390 u.save!
432 u.save!
391
433
392 put :update, :id => u.id, :user => {:auth_source_id => '', :password => 'newpass123', :password_confirmation => 'newpass123'}
434 put :update, :id => u.id, :user => {:auth_source_id => '', :password => 'newpass123', :password_confirmation => 'newpass123'}
393
435
394 assert_equal nil, u.reload.auth_source
436 assert_equal nil, u.reload.auth_source
395 assert u.check_password?('newpass123')
437 assert u.check_password?('newpass123')
396 end
438 end
397
439
398 def test_update_notified_project
440 def test_update_notified_project
399 get :edit, :id => 2
441 get :edit, :id => 2
400 assert_response :success
442 assert_response :success
401 assert_template 'edit'
443 assert_template 'edit'
402 u = User.find(2)
444 u = User.find(2)
403 assert_equal [1, 2, 5], u.projects.collect{|p| p.id}.sort
445 assert_equal [1, 2, 5], u.projects.collect{|p| p.id}.sort
404 assert_equal [1, 2, 5], u.notified_projects_ids.sort
446 assert_equal [1, 2, 5], u.notified_projects_ids.sort
405 assert_select 'input[name=?][value=?]', 'user[notified_project_ids][]', '1'
447 assert_select 'input[name=?][value=?]', 'user[notified_project_ids][]', '1'
406 assert_equal 'all', u.mail_notification
448 assert_equal 'all', u.mail_notification
407 put :update, :id => 2,
449 put :update, :id => 2,
408 :user => {
450 :user => {
409 :mail_notification => 'selected',
451 :mail_notification => 'selected',
410 :notified_project_ids => [1, 2]
452 :notified_project_ids => [1, 2]
411 }
453 }
412 u = User.find(2)
454 u = User.find(2)
413 assert_equal 'selected', u.mail_notification
455 assert_equal 'selected', u.mail_notification
414 assert_equal [1, 2], u.notified_projects_ids.sort
456 assert_equal [1, 2], u.notified_projects_ids.sort
415 end
457 end
416
458
417 def test_update_status_should_not_update_attributes
459 def test_update_status_should_not_update_attributes
418 user = User.find(2)
460 user = User.find(2)
419 user.pref[:no_self_notified] = '1'
461 user.pref[:no_self_notified] = '1'
420 user.pref.save
462 user.pref.save
421
463
422 put :update, :id => 2, :user => {:status => 3}
464 put :update, :id => 2, :user => {:status => 3}
423 assert_response 302
465 assert_response 302
424 user = User.find(2)
466 user = User.find(2)
425 assert_equal 3, user.status
467 assert_equal 3, user.status
426 assert_equal '1', user.pref[:no_self_notified]
468 assert_equal '1', user.pref[:no_self_notified]
427 end
469 end
428
470
471 def test_update_assign_admin_should_send_security_notification
472 ActionMailer::Base.deliveries.clear
473 put :update, :id => 2, :user => {
474 :admin => 1
475 }
476
477 assert_not_nil (mail = ActionMailer::Base.deliveries.last)
478 assert_mail_body_match I18n.t(:mail_body_security_notification_add, field: I18n.t(:field_admin), value: User.find(2).login), mail
479
480 # All admins should receive this
481 User.where(admin: true, status: Principal::STATUS_ACTIVE).each do |admin|
482 assert_not_nil ActionMailer::Base.deliveries.detect{|mail| [mail.bcc, mail.cc].flatten.include?(admin.mail) }
483 end
484 end
485
486 def test_update_unassign_admin_should_send_security_notification
487 user = User.find(2)
488 user.admin = true
489 user.save!
490
491 ActionMailer::Base.deliveries.clear
492 put :update, :id => user.id, :user => {
493 :admin => 0
494 }
495
496 assert_not_nil (mail = ActionMailer::Base.deliveries.last)
497 assert_mail_body_match I18n.t(:mail_body_security_notification_remove, field: I18n.t(:field_admin), value: user.login), mail
498
499 # All admins should receive this
500 User.where(admin: true, status: Principal::STATUS_ACTIVE).each do |admin|
501 assert_not_nil ActionMailer::Base.deliveries.detect{|mail| [mail.bcc, mail.cc].flatten.include?(admin.mail) }
502 end
503 end
504
505 def test_update_lock_admin_should_send_security_notification
506 user = User.find(2)
507 user.admin = true
508 user.save!
509
510 ActionMailer::Base.deliveries.clear
511 put :update, :id => 2, :user => {
512 :status => Principal::STATUS_LOCKED
513 }
514
515 assert_not_nil (mail = ActionMailer::Base.deliveries.last)
516 assert_mail_body_match I18n.t(:mail_body_security_notification_remove, field: I18n.t(:field_admin), value: User.find(2).login), mail
517
518 # All admins should receive this
519 User.where(admin: true, status: Principal::STATUS_ACTIVE).each do |admin|
520 assert_not_nil ActionMailer::Base.deliveries.detect{|mail| [mail.bcc, mail.cc].flatten.include?(admin.mail) }
521 end
522
523 # if user is already locked, destroying should not send a second mail
524 # (for active admins see furtherbelow)
525 ActionMailer::Base.deliveries.clear
526 delete :destroy, :id => 1
527 assert_nil ActionMailer::Base.deliveries.last
528
529 end
530
531 def test_update_unlock_admin_should_send_security_notification
532 user = User.find(5) # already locked
533 user.admin = true
534 user.save!
535 ActionMailer::Base.deliveries.clear
536 put :update, :id => user.id, :user => {
537 :status => Principal::STATUS_ACTIVE
538 }
539
540 assert_not_nil (mail = ActionMailer::Base.deliveries.last)
541 assert_mail_body_match I18n.t(:mail_body_security_notification_add, field: I18n.t(:field_admin), value: user.login), mail
542
543 # All admins should receive this
544 User.where(admin: true, status: Principal::STATUS_ACTIVE).each do |admin|
545 assert_not_nil ActionMailer::Base.deliveries.detect{|mail| [mail.bcc, mail.cc].flatten.include?(admin.mail) }
546 end
547 end
548
549 def test_update_admin_unrelated_property_should_not_send_security_notification
550 ActionMailer::Base.deliveries.clear
551 put :update, :id => 1, :user => {
552 :firstname => 'Jimmy'
553 }
554 assert_nil ActionMailer::Base.deliveries.last
555 end
556
429 def test_destroy
557 def test_destroy
430 assert_difference 'User.count', -1 do
558 assert_difference 'User.count', -1 do
431 delete :destroy, :id => 2
559 delete :destroy, :id => 2
432 end
560 end
433 assert_redirected_to '/users'
561 assert_redirected_to '/users'
434 assert_nil User.find_by_id(2)
562 assert_nil User.find_by_id(2)
435 end
563 end
436
564
437 def test_destroy_should_be_denied_for_non_admin_users
565 def test_destroy_should_be_denied_for_non_admin_users
438 @request.session[:user_id] = 3
566 @request.session[:user_id] = 3
439
567
440 assert_no_difference 'User.count' do
568 assert_no_difference 'User.count' do
441 get :destroy, :id => 2
569 get :destroy, :id => 2
442 end
570 end
443 assert_response 403
571 assert_response 403
444 end
572 end
445
573
446 def test_destroy_should_redirect_to_back_url_param
574 def test_destroy_should_redirect_to_back_url_param
447 assert_difference 'User.count', -1 do
575 assert_difference 'User.count', -1 do
448 delete :destroy, :id => 2, :back_url => '/users?name=foo'
576 delete :destroy, :id => 2, :back_url => '/users?name=foo'
449 end
577 end
450 assert_redirected_to '/users?name=foo'
578 assert_redirected_to '/users?name=foo'
451 end
579 end
580
581 def test_destroy_active_admin_should_send_security_notification
582 user = User.find(2)
583 user.admin = true
584 user.save!
585 ActionMailer::Base.deliveries.clear
586 delete :destroy, :id => user.id
587
588 assert_not_nil (mail = ActionMailer::Base.deliveries.last)
589 assert_mail_body_match I18n.t(:mail_body_security_notification_remove, field: I18n.t(:field_admin), value: user.login), mail
590
591 # All admins should receive this
592 User.where(admin: true, status: Principal::STATUS_ACTIVE).each do |admin|
593 assert_not_nil ActionMailer::Base.deliveries.detect{|mail| [mail.bcc, mail.cc].flatten.include?(admin.mail) }
594 end
595 end
452 end
596 end
General Comments 0
You need to be logged in to leave comments. Login now