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