##// END OF EJS Templates
Force string comparison for login search to be case sensitive on MySQL. #2473...
Eric Davis -
r3699:672852baafdd
parent child
Show More
@@ -1,379 +1,382
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require "digest/sha1"
19 19
20 20 class User < Principal
21 21
22 22 # Account statuses
23 23 STATUS_ANONYMOUS = 0
24 24 STATUS_ACTIVE = 1
25 25 STATUS_REGISTERED = 2
26 26 STATUS_LOCKED = 3
27 27
28 28 USER_FORMATS = {
29 29 :firstname_lastname => '#{firstname} #{lastname}',
30 30 :firstname => '#{firstname}',
31 31 :lastname_firstname => '#{lastname} #{firstname}',
32 32 :lastname_coma_firstname => '#{lastname}, #{firstname}',
33 33 :username => '#{login}'
34 34 }
35 35
36 36 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
37 37 :after_remove => Proc.new {|user, group| group.user_removed(user)}
38 38 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
39 39 has_many :changesets, :dependent => :nullify
40 40 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
41 41 has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
42 42 has_one :api_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='api'"
43 43 belongs_to :auth_source
44 44
45 45 # Active non-anonymous users scope
46 46 named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
47 47
48 48 acts_as_customizable
49 49
50 50 attr_accessor :password, :password_confirmation
51 51 attr_accessor :last_before_login_on
52 52 # Prevents unauthorized assignments
53 53 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password, :group_ids
54 54
55 55 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
56 56 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }, :case_sensitive => false
57 57 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
58 58 # Login must contain lettres, numbers, underscores only
59 59 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
60 60 validates_length_of :login, :maximum => 30
61 61 validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-\.]*$/i
62 62 validates_length_of :firstname, :lastname, :maximum => 30
63 63 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
64 64 validates_length_of :mail, :maximum => 60, :allow_nil => true
65 65 validates_confirmation_of :password, :allow_nil => true
66 66
67 67 def before_create
68 68 self.mail_notification = false
69 69 true
70 70 end
71 71
72 72 def before_save
73 73 # update hashed_password if password was set
74 74 self.hashed_password = User.hash_password(self.password) if self.password && self.auth_source_id.blank?
75 75 end
76 76
77 77 def reload(*args)
78 78 @name = nil
79 79 super
80 80 end
81 81
82 82 def identity_url=(url)
83 83 if url.blank?
84 84 write_attribute(:identity_url, '')
85 85 else
86 86 begin
87 87 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
88 88 rescue OpenIdAuthentication::InvalidOpenId
89 89 # Invlaid url, don't save
90 90 end
91 91 end
92 92 self.read_attribute(:identity_url)
93 93 end
94 94
95 95 # Returns the user that matches provided login and password, or nil
96 96 def self.try_to_login(login, password)
97 97 # Make sure no one can sign in with an empty password
98 98 return nil if password.to_s.empty?
99 99 user = find_by_login(login)
100 100 if user
101 101 # user is already in local database
102 102 return nil if !user.active?
103 103 if user.auth_source
104 104 # user has an external authentication method
105 105 return nil unless user.auth_source.authenticate(login, password)
106 106 else
107 107 # authentication with local password
108 108 return nil unless User.hash_password(password) == user.hashed_password
109 109 end
110 110 else
111 111 # user is not yet registered, try to authenticate with available sources
112 112 attrs = AuthSource.authenticate(login, password)
113 113 if attrs
114 114 user = new(attrs)
115 115 user.login = login
116 116 user.language = Setting.default_language
117 117 if user.save
118 118 user.reload
119 119 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
120 120 end
121 121 end
122 122 end
123 123 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
124 124 user
125 125 rescue => text
126 126 raise text
127 127 end
128 128
129 129 # Returns the user who matches the given autologin +key+ or nil
130 130 def self.try_to_autologin(key)
131 131 tokens = Token.find_all_by_action_and_value('autologin', key)
132 132 # Make sure there's only 1 token that matches the key
133 133 if tokens.size == 1
134 134 token = tokens.first
135 135 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
136 136 token.user.update_attribute(:last_login_on, Time.now)
137 137 token.user
138 138 end
139 139 end
140 140 end
141 141
142 142 # Return user's full name for display
143 143 def name(formatter = nil)
144 144 if formatter
145 145 eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
146 146 else
147 147 @name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
148 148 end
149 149 end
150 150
151 151 def active?
152 152 self.status == STATUS_ACTIVE
153 153 end
154 154
155 155 def registered?
156 156 self.status == STATUS_REGISTERED
157 157 end
158 158
159 159 def locked?
160 160 self.status == STATUS_LOCKED
161 161 end
162 162
163 163 def check_password?(clear_password)
164 164 if auth_source_id.present?
165 165 auth_source.authenticate(self.login, clear_password)
166 166 else
167 167 User.hash_password(clear_password) == self.hashed_password
168 168 end
169 169 end
170 170
171 171 # Does the backend storage allow this user to change their password?
172 172 def change_password_allowed?
173 173 return true if auth_source_id.blank?
174 174 return auth_source.allow_password_changes?
175 175 end
176 176
177 177 # Generate and set a random password. Useful for automated user creation
178 178 # Based on Token#generate_token_value
179 179 #
180 180 def random_password
181 181 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
182 182 password = ''
183 183 40.times { |i| password << chars[rand(chars.size-1)] }
184 184 self.password = password
185 185 self.password_confirmation = password
186 186 self
187 187 end
188 188
189 189 def pref
190 190 self.preference ||= UserPreference.new(:user => self)
191 191 end
192 192
193 193 def time_zone
194 194 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
195 195 end
196 196
197 197 def wants_comments_in_reverse_order?
198 198 self.pref[:comments_sorting] == 'desc'
199 199 end
200 200
201 201 # Return user's RSS key (a 40 chars long string), used to access feeds
202 202 def rss_key
203 203 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
204 204 token.value
205 205 end
206 206
207 207 # Return user's API key (a 40 chars long string), used to access the API
208 208 def api_key
209 209 token = self.api_token || self.create_api_token(:action => 'api')
210 210 token.value
211 211 end
212 212
213 213 # Return an array of project ids for which the user has explicitly turned mail notifications on
214 214 def notified_projects_ids
215 215 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
216 216 end
217 217
218 218 def notified_project_ids=(ids)
219 219 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
220 220 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
221 221 @notified_projects_ids = nil
222 222 notified_projects_ids
223 223 end
224 224
225 225 # Find a user account by matching the exact login and then a case-insensitive
226 226 # version. Exact matches will be given priority.
227 227 def self.find_by_login(login)
228 # force string comparison to be case sensitive on MySQL
229 type_cast = (ActiveRecord::Base.connection.adapter_name == 'MySQL') ? 'BINARY' : ''
230
228 231 # First look for an exact match
229 user = first(:conditions => {:login => login})
232 user = first(:conditions => ["#{type_cast} login = ?", login])
230 233 # Fail over to case-insensitive if none was found
231 user ||= first(:conditions => ["LOWER(login) = ?", login.to_s.downcase])
234 user ||= first(:conditions => ["#{type_cast} LOWER(login) = ?", login.to_s.downcase])
232 235 end
233 236
234 237 def self.find_by_rss_key(key)
235 238 token = Token.find_by_value(key)
236 239 token && token.user.active? ? token.user : nil
237 240 end
238 241
239 242 def self.find_by_api_key(key)
240 243 token = Token.find_by_action_and_value('api', key)
241 244 token && token.user.active? ? token.user : nil
242 245 end
243 246
244 247 # Makes find_by_mail case-insensitive
245 248 def self.find_by_mail(mail)
246 249 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
247 250 end
248 251
249 252 def to_s
250 253 name
251 254 end
252 255
253 256 # Returns the current day according to user's time zone
254 257 def today
255 258 if time_zone.nil?
256 259 Date.today
257 260 else
258 261 Time.now.in_time_zone(time_zone).to_date
259 262 end
260 263 end
261 264
262 265 def logged?
263 266 true
264 267 end
265 268
266 269 def anonymous?
267 270 !logged?
268 271 end
269 272
270 273 # Return user's roles for project
271 274 def roles_for_project(project)
272 275 roles = []
273 276 # No role on archived projects
274 277 return roles unless project && project.active?
275 278 if logged?
276 279 # Find project membership
277 280 membership = memberships.detect {|m| m.project_id == project.id}
278 281 if membership
279 282 roles = membership.roles
280 283 else
281 284 @role_non_member ||= Role.non_member
282 285 roles << @role_non_member
283 286 end
284 287 else
285 288 @role_anonymous ||= Role.anonymous
286 289 roles << @role_anonymous
287 290 end
288 291 roles
289 292 end
290 293
291 294 # Return true if the user is a member of project
292 295 def member_of?(project)
293 296 !roles_for_project(project).detect {|role| role.member?}.nil?
294 297 end
295 298
296 299 # Return true if the user is allowed to do the specified action on project
297 300 # action can be:
298 301 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
299 302 # * a permission Symbol (eg. :edit_project)
300 303 def allowed_to?(action, project, options={})
301 304 if project
302 305 # No action allowed on archived projects
303 306 return false unless project.active?
304 307 # No action allowed on disabled modules
305 308 return false unless project.allows_to?(action)
306 309 # Admin users are authorized for anything else
307 310 return true if admin?
308 311
309 312 roles = roles_for_project(project)
310 313 return false unless roles
311 314 roles.detect {|role| (project.is_public? || role.member?) && role.allowed_to?(action)}
312 315
313 316 elsif options[:global]
314 317 # Admin users are always authorized
315 318 return true if admin?
316 319
317 320 # authorize if user has at least one role that has this permission
318 321 roles = memberships.collect {|m| m.roles}.flatten.uniq
319 322 roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
320 323 else
321 324 false
322 325 end
323 326 end
324 327
325 328 def self.current=(user)
326 329 @current_user = user
327 330 end
328 331
329 332 def self.current
330 333 @current_user ||= User.anonymous
331 334 end
332 335
333 336 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
334 337 # one anonymous user per database.
335 338 def self.anonymous
336 339 anonymous_user = AnonymousUser.find(:first)
337 340 if anonymous_user.nil?
338 341 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
339 342 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
340 343 end
341 344 anonymous_user
342 345 end
343 346
344 347 protected
345 348
346 349 def validate
347 350 # Password length validation based on setting
348 351 if !password.nil? && password.size < Setting.password_min_length.to_i
349 352 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
350 353 end
351 354 end
352 355
353 356 private
354 357
355 358 # Return password digest
356 359 def self.hash_password(clear_password)
357 360 Digest::SHA1.hexdigest(clear_password || "")
358 361 end
359 362 end
360 363
361 364 class AnonymousUser < User
362 365
363 366 def validate_on_create
364 367 # There should be only one AnonymousUser in the database
365 368 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
366 369 end
367 370
368 371 def available_custom_fields
369 372 []
370 373 end
371 374
372 375 # Overrides a few properties
373 376 def logged?; false end
374 377 def admin; false end
375 378 def name(*args); I18n.t(:label_user_anonymous) end
376 379 def mail; nil end
377 380 def time_zone; nil end
378 381 def rss_key; nil end
379 382 end
General Comments 0
You need to be logged in to leave comments. Login now