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