##// END OF EJS Templates
Makes User.find_by_mail case-insensitive (password reminder #2322, repo users mapping)....
Jean-Philippe Lang -
r2120:72d0843c1fef
parent child
Show More
@@ -1,289 +1,294
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 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 < ActiveRecord::Base
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_many :memberships, :class_name => 'Member', :include => [ :project, :role ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name"
37 37 has_many :members, :dependent => :delete_all
38 38 has_many :projects, :through => :memberships
39 39 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
40 40 has_many :changesets, :dependent => :nullify
41 41 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
42 42 has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
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
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? }
57 57 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }
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_length_of :password, :minimum => 4, :allow_nil => true
66 66 validates_confirmation_of :password, :allow_nil => true
67 67
68 68 def before_create
69 69 self.mail_notification = false
70 70 true
71 71 end
72 72
73 73 def before_save
74 74 # update hashed_password if password was set
75 75 self.hashed_password = User.hash_password(self.password) if self.password
76 76 end
77 77
78 78 def reload(*args)
79 79 @name = nil
80 80 super
81 81 end
82 82
83 83 # Returns the user that matches provided login and password, or nil
84 84 def self.try_to_login(login, password)
85 85 # Make sure no one can sign in with an empty password
86 86 return nil if password.to_s.empty?
87 87 user = find(:first, :conditions => ["login=?", login])
88 88 if user
89 89 # user is already in local database
90 90 return nil if !user.active?
91 91 if user.auth_source
92 92 # user has an external authentication method
93 93 return nil unless user.auth_source.authenticate(login, password)
94 94 else
95 95 # authentication with local password
96 96 return nil unless User.hash_password(password) == user.hashed_password
97 97 end
98 98 else
99 99 # user is not yet registered, try to authenticate with available sources
100 100 attrs = AuthSource.authenticate(login, password)
101 101 if attrs
102 102 user = new(*attrs)
103 103 user.login = login
104 104 user.language = Setting.default_language
105 105 if user.save
106 106 user.reload
107 107 logger.info("User '#{user.login}' created from the LDAP") if logger
108 108 end
109 109 end
110 110 end
111 111 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
112 112 user
113 113 rescue => text
114 114 raise text
115 115 end
116 116
117 117 # Return user's full name for display
118 118 def name(formatter = nil)
119 119 if formatter
120 120 eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
121 121 else
122 122 @name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
123 123 end
124 124 end
125 125
126 126 def active?
127 127 self.status == STATUS_ACTIVE
128 128 end
129 129
130 130 def registered?
131 131 self.status == STATUS_REGISTERED
132 132 end
133 133
134 134 def locked?
135 135 self.status == STATUS_LOCKED
136 136 end
137 137
138 138 def check_password?(clear_password)
139 139 User.hash_password(clear_password) == self.hashed_password
140 140 end
141 141
142 142 def pref
143 143 self.preference ||= UserPreference.new(:user => self)
144 144 end
145 145
146 146 def time_zone
147 147 @time_zone ||= (self.pref.time_zone.blank? ? nil : TimeZone[self.pref.time_zone])
148 148 end
149 149
150 150 def wants_comments_in_reverse_order?
151 151 self.pref[:comments_sorting] == 'desc'
152 152 end
153 153
154 154 # Return user's RSS key (a 40 chars long string), used to access feeds
155 155 def rss_key
156 156 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
157 157 token.value
158 158 end
159 159
160 160 # Return an array of project ids for which the user has explicitly turned mail notifications on
161 161 def notified_projects_ids
162 162 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
163 163 end
164 164
165 165 def notified_project_ids=(ids)
166 166 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
167 167 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
168 168 @notified_projects_ids = nil
169 169 notified_projects_ids
170 170 end
171 171
172 172 def self.find_by_rss_key(key)
173 173 token = Token.find_by_value(key)
174 174 token && token.user.active? ? token.user : nil
175 175 end
176 176
177 177 def self.find_by_autologin_key(key)
178 178 token = Token.find_by_action_and_value('autologin', key)
179 179 token && (token.created_on > Setting.autologin.to_i.day.ago) && token.user.active? ? token.user : nil
180 180 end
181 181
182 # Makes find_by_mail case-insensitive
183 def self.find_by_mail(mail)
184 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
185 end
186
182 187 # Sort users by their display names
183 188 def <=>(user)
184 189 self.to_s.downcase <=> user.to_s.downcase
185 190 end
186 191
187 192 def to_s
188 193 name
189 194 end
190 195
191 196 def logged?
192 197 true
193 198 end
194 199
195 200 def anonymous?
196 201 !logged?
197 202 end
198 203
199 204 # Return user's role for project
200 205 def role_for_project(project)
201 206 # No role on archived projects
202 207 return nil unless project && project.active?
203 208 if logged?
204 209 # Find project membership
205 210 membership = memberships.detect {|m| m.project_id == project.id}
206 211 if membership
207 212 membership.role
208 213 else
209 214 @role_non_member ||= Role.non_member
210 215 end
211 216 else
212 217 @role_anonymous ||= Role.anonymous
213 218 end
214 219 end
215 220
216 221 # Return true if the user is a member of project
217 222 def member_of?(project)
218 223 role_for_project(project).member?
219 224 end
220 225
221 226 # Return true if the user is allowed to do the specified action on project
222 227 # action can be:
223 228 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
224 229 # * a permission Symbol (eg. :edit_project)
225 230 def allowed_to?(action, project, options={})
226 231 if project
227 232 # No action allowed on archived projects
228 233 return false unless project.active?
229 234 # No action allowed on disabled modules
230 235 return false unless project.allows_to?(action)
231 236 # Admin users are authorized for anything else
232 237 return true if admin?
233 238
234 239 role = role_for_project(project)
235 240 return false unless role
236 241 role.allowed_to?(action) && (project.is_public? || role.member?)
237 242
238 243 elsif options[:global]
239 244 # authorize if user has at least one role that has this permission
240 245 roles = memberships.collect {|m| m.role}.uniq
241 246 roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
242 247 else
243 248 false
244 249 end
245 250 end
246 251
247 252 def self.current=(user)
248 253 @current_user = user
249 254 end
250 255
251 256 def self.current
252 257 @current_user ||= User.anonymous
253 258 end
254 259
255 260 def self.anonymous
256 261 anonymous_user = AnonymousUser.find(:first)
257 262 if anonymous_user.nil?
258 263 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
259 264 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
260 265 end
261 266 anonymous_user
262 267 end
263 268
264 269 private
265 270 # Return password digest
266 271 def self.hash_password(clear_password)
267 272 Digest::SHA1.hexdigest(clear_password || "")
268 273 end
269 274 end
270 275
271 276 class AnonymousUser < User
272 277
273 278 def validate_on_create
274 279 # There should be only one AnonymousUser in the database
275 280 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
276 281 end
277 282
278 283 def available_custom_fields
279 284 []
280 285 end
281 286
282 287 # Overrides a few properties
283 288 def logged?; false end
284 289 def admin; false end
285 290 def name; 'Anonymous' end
286 291 def mail; nil end
287 292 def time_zone; nil end
288 293 def rss_key; nil end
289 294 end
@@ -1,153 +1,153
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 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 "#{File.dirname(__FILE__)}/../test_helper"
19 19
20 20 begin
21 21 require 'mocha'
22 22 rescue
23 23 # Won't run some tests
24 24 end
25 25
26 26 class AccountTest < ActionController::IntegrationTest
27 27 fixtures :users
28 28
29 29 # Replace this with your real tests.
30 30 def test_login
31 31 get "my/page"
32 32 assert_redirected_to "account/login"
33 33 log_user('jsmith', 'jsmith')
34 34
35 35 get "my/account"
36 36 assert_response :success
37 37 assert_template "my/account"
38 38 end
39 39
40 40 def test_lost_password
41 41 Token.delete_all
42 42
43 43 get "account/lost_password"
44 44 assert_response :success
45 45 assert_template "account/lost_password"
46 46
47 post "account/lost_password", :mail => 'jsmith@somenet.foo'
47 post "account/lost_password", :mail => 'jSmith@somenet.foo'
48 48 assert_redirected_to "account/login"
49 49
50 50 token = Token.find(:first)
51 51 assert_equal 'recovery', token.action
52 52 assert_equal 'jsmith@somenet.foo', token.user.mail
53 53 assert !token.expired?
54 54
55 55 get "account/lost_password", :token => token.value
56 56 assert_response :success
57 57 assert_template "account/password_recovery"
58 58
59 59 post "account/lost_password", :token => token.value, :new_password => 'newpass', :new_password_confirmation => 'newpass'
60 60 assert_redirected_to "account/login"
61 61 assert_equal 'Password was successfully updated.', flash[:notice]
62 62
63 63 log_user('jsmith', 'newpass')
64 64 assert_equal 0, Token.count
65 65 end
66 66
67 67 def test_register_with_automatic_activation
68 68 Setting.self_registration = '3'
69 69
70 70 get 'account/register'
71 71 assert_response :success
72 72 assert_template 'account/register'
73 73
74 74 post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar"},
75 75 :password => "newpass", :password_confirmation => "newpass"
76 76 assert_redirected_to 'my/account'
77 77 follow_redirect!
78 78 assert_response :success
79 79 assert_template 'my/account'
80 80
81 81 assert User.find_by_login('newuser').active?
82 82 end
83 83
84 84 def test_register_with_manual_activation
85 85 Setting.self_registration = '2'
86 86
87 87 post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar"},
88 88 :password => "newpass", :password_confirmation => "newpass"
89 89 assert_redirected_to 'account/login'
90 90 assert !User.find_by_login('newuser').active?
91 91 end
92 92
93 93 def test_register_with_email_activation
94 94 Setting.self_registration = '1'
95 95 Token.delete_all
96 96
97 97 post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar"},
98 98 :password => "newpass", :password_confirmation => "newpass"
99 99 assert_redirected_to 'account/login'
100 100 assert !User.find_by_login('newuser').active?
101 101
102 102 token = Token.find(:first)
103 103 assert_equal 'register', token.action
104 104 assert_equal 'newuser@foo.bar', token.user.mail
105 105 assert !token.expired?
106 106
107 107 get 'account/activate', :token => token.value
108 108 assert_redirected_to 'account/login'
109 109 log_user('newuser', 'newpass')
110 110 end
111 111
112 112 if Object.const_defined?(:Mocha)
113 113
114 114 def test_onthefly_registration
115 115 # disable registration
116 116 Setting.self_registration = '0'
117 117 AuthSource.expects(:authenticate).returns([:login => 'foo', :firstname => 'Foo', :lastname => 'Smith', :mail => 'foo@bar.com', :auth_source_id => 66])
118 118
119 119 post 'account/login', :username => 'foo', :password => 'bar'
120 120 assert_redirected_to 'my/page'
121 121
122 122 user = User.find_by_login('foo')
123 123 assert user.is_a?(User)
124 124 assert_equal 66, user.auth_source_id
125 125 assert user.hashed_password.blank?
126 126 end
127 127
128 128 def test_onthefly_registration_with_invalid_attributes
129 129 # disable registration
130 130 Setting.self_registration = '0'
131 131 AuthSource.expects(:authenticate).returns([:login => 'foo', :lastname => 'Smith', :auth_source_id => 66])
132 132
133 133 post 'account/login', :username => 'foo', :password => 'bar'
134 134 assert_response :success
135 135 assert_template 'account/register'
136 136 assert_tag :input, :attributes => { :name => 'user[firstname]', :value => '' }
137 137 assert_tag :input, :attributes => { :name => 'user[lastname]', :value => 'Smith' }
138 138 assert_no_tag :input, :attributes => { :name => 'user[login]' }
139 139 assert_no_tag :input, :attributes => { :name => 'user[password]' }
140 140
141 141 post 'account/register', :user => {:firstname => 'Foo', :lastname => 'Smith', :mail => 'foo@bar.com'}
142 142 assert_redirected_to 'my/account'
143 143
144 144 user = User.find_by_login('foo')
145 145 assert user.is_a?(User)
146 146 assert_equal 66, user.auth_source_id
147 147 assert user.hashed_password.blank?
148 148 end
149 149
150 150 else
151 151 puts 'Mocha is missing. Skipping tests.'
152 152 end
153 153 end
@@ -1,161 +1,167
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 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 File.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class UserTest < Test::Unit::TestCase
21 21 fixtures :users, :members, :projects
22 22
23 23 def setup
24 24 @admin = User.find(1)
25 25 @jsmith = User.find(2)
26 26 @dlopper = User.find(3)
27 27 end
28 28
29 29 def test_truth
30 30 assert_kind_of User, @jsmith
31 31 end
32 32
33 33 def test_create
34 34 user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
35 35
36 36 user.login = "jsmith"
37 37 user.password, user.password_confirmation = "password", "password"
38 38 # login uniqueness
39 39 assert !user.save
40 40 assert_equal 1, user.errors.count
41 41
42 42 user.login = "newuser"
43 43 user.password, user.password_confirmation = "passwd", "password"
44 44 # password confirmation
45 45 assert !user.save
46 46 assert_equal 1, user.errors.count
47 47
48 48 user.password, user.password_confirmation = "password", "password"
49 49 assert user.save
50 50 end
51 51
52 52 def test_update
53 53 assert_equal "admin", @admin.login
54 54 @admin.login = "john"
55 55 assert @admin.save, @admin.errors.full_messages.join("; ")
56 56 @admin.reload
57 57 assert_equal "john", @admin.login
58 58 end
59 59
60 60 def test_destroy
61 61 User.find(2).destroy
62 62 assert_nil User.find_by_id(2)
63 63 assert Member.find_all_by_user_id(2).empty?
64 64 end
65 65
66 66 def test_validate
67 67 @admin.login = ""
68 68 assert !@admin.save
69 69 assert_equal 1, @admin.errors.count
70 70 end
71 71
72 72 def test_password
73 73 user = User.try_to_login("admin", "admin")
74 74 assert_kind_of User, user
75 75 assert_equal "admin", user.login
76 76 user.password = "hello"
77 77 assert user.save
78 78
79 79 user = User.try_to_login("admin", "hello")
80 80 assert_kind_of User, user
81 81 assert_equal "admin", user.login
82 82 assert_equal User.hash_password("hello"), user.hashed_password
83 83 end
84 84
85 85 def test_name_format
86 86 assert_equal 'Smith, John', @jsmith.name(:lastname_coma_firstname)
87 87 Setting.user_format = :firstname_lastname
88 88 assert_equal 'John Smith', @jsmith.reload.name
89 89 Setting.user_format = :username
90 90 assert_equal 'jsmith', @jsmith.reload.name
91 91 end
92 92
93 93 def test_lock
94 94 user = User.try_to_login("jsmith", "jsmith")
95 95 assert_equal @jsmith, user
96 96
97 97 @jsmith.status = User::STATUS_LOCKED
98 98 assert @jsmith.save
99 99
100 100 user = User.try_to_login("jsmith", "jsmith")
101 101 assert_equal nil, user
102 102 end
103 103
104 104 def test_create_anonymous
105 105 AnonymousUser.delete_all
106 106 anon = User.anonymous
107 107 assert !anon.new_record?
108 108 assert_kind_of AnonymousUser, anon
109 109 end
110 110
111 111 def test_rss_key
112 112 assert_nil @jsmith.rss_token
113 113 key = @jsmith.rss_key
114 114 assert_equal 40, key.length
115 115
116 116 @jsmith.reload
117 117 assert_equal key, @jsmith.rss_key
118 118 end
119 119
120 120 def test_role_for_project
121 121 # user with a role
122 122 role = @jsmith.role_for_project(Project.find(1))
123 123 assert_kind_of Role, role
124 124 assert_equal "Manager", role.name
125 125
126 126 # user with no role
127 127 assert !@dlopper.role_for_project(Project.find(2)).member?
128 128 end
129 129
130 130 def test_mail_notification_all
131 131 @jsmith.mail_notification = true
132 132 @jsmith.notified_project_ids = []
133 133 @jsmith.save
134 134 @jsmith.reload
135 135 assert @jsmith.projects.first.recipients.include?(@jsmith.mail)
136 136 end
137 137
138 138 def test_mail_notification_selected
139 139 @jsmith.mail_notification = false
140 140 @jsmith.notified_project_ids = [1]
141 141 @jsmith.save
142 142 @jsmith.reload
143 143 assert Project.find(1).recipients.include?(@jsmith.mail)
144 144 end
145 145
146 146 def test_mail_notification_none
147 147 @jsmith.mail_notification = false
148 148 @jsmith.notified_project_ids = []
149 149 @jsmith.save
150 150 @jsmith.reload
151 151 assert !@jsmith.projects.first.recipients.include?(@jsmith.mail)
152 152 end
153 153
154 154 def test_comments_sorting_preference
155 155 assert !@jsmith.wants_comments_in_reverse_order?
156 156 @jsmith.pref.comments_sorting = 'asc'
157 157 assert !@jsmith.wants_comments_in_reverse_order?
158 158 @jsmith.pref.comments_sorting = 'desc'
159 159 assert @jsmith.wants_comments_in_reverse_order?
160 160 end
161
162 def test_find_by_mail_should_be_case_insensitive
163 u = User.find_by_mail('JSmith@somenet.foo')
164 assert_not_nil u
165 assert_equal 'jsmith@somenet.foo', u.mail
166 end
161 167 end
General Comments 0
You need to be logged in to leave comments. Login now