##// END OF EJS Templates
Fixed: User#identity_url raises an error when invalid url is supplied (#2742)....
Jean-Philippe Lang -
r2415:d643d9a94c65
parent child
Show More
@@ -1,315 +1,319
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? }, :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_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 def identity_url=(url)
84 begin
85 self.write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
86 rescue InvalidOpenId
87 # Invlaid url, don't save
84 if url.blank?
85 write_attribute(:identity_url, '')
86 else
87 begin
88 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
89 rescue OpenIdAuthentication::InvalidOpenId
90 # Invlaid url, don't save
91 end
88 92 end
89 93 self.read_attribute(:identity_url)
90 94 end
91 95
92 96 # Returns the user that matches provided login and password, or nil
93 97 def self.try_to_login(login, password)
94 98 # Make sure no one can sign in with an empty password
95 99 return nil if password.to_s.empty?
96 100 user = find(:first, :conditions => ["login=?", login])
97 101 if user
98 102 # user is already in local database
99 103 return nil if !user.active?
100 104 if user.auth_source
101 105 # user has an external authentication method
102 106 return nil unless user.auth_source.authenticate(login, password)
103 107 else
104 108 # authentication with local password
105 109 return nil unless User.hash_password(password) == user.hashed_password
106 110 end
107 111 else
108 112 # user is not yet registered, try to authenticate with available sources
109 113 attrs = AuthSource.authenticate(login, password)
110 114 if attrs
111 115 user = new(*attrs)
112 116 user.login = login
113 117 user.language = Setting.default_language
114 118 if user.save
115 119 user.reload
116 120 logger.info("User '#{user.login}' created from the LDAP") if logger
117 121 end
118 122 end
119 123 end
120 124 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
121 125 user
122 126 rescue => text
123 127 raise text
124 128 end
125 129
126 130 # Return user's full name for display
127 131 def name(formatter = nil)
128 132 if formatter
129 133 eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
130 134 else
131 135 @name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
132 136 end
133 137 end
134 138
135 139 def active?
136 140 self.status == STATUS_ACTIVE
137 141 end
138 142
139 143 def registered?
140 144 self.status == STATUS_REGISTERED
141 145 end
142 146
143 147 def locked?
144 148 self.status == STATUS_LOCKED
145 149 end
146 150
147 151 def check_password?(clear_password)
148 152 User.hash_password(clear_password) == self.hashed_password
149 153 end
150 154
151 155 # Generate and set a random password. Useful for automated user creation
152 156 # Based on Token#generate_token_value
153 157 #
154 158 def random_password
155 159 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
156 160 password = ''
157 161 40.times { |i| password << chars[rand(chars.size-1)] }
158 162 self.password = password
159 163 self.password_confirmation = password
160 164 self
161 165 end
162 166
163 167 def pref
164 168 self.preference ||= UserPreference.new(:user => self)
165 169 end
166 170
167 171 def time_zone
168 172 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
169 173 end
170 174
171 175 def wants_comments_in_reverse_order?
172 176 self.pref[:comments_sorting] == 'desc'
173 177 end
174 178
175 179 # Return user's RSS key (a 40 chars long string), used to access feeds
176 180 def rss_key
177 181 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
178 182 token.value
179 183 end
180 184
181 185 # Return an array of project ids for which the user has explicitly turned mail notifications on
182 186 def notified_projects_ids
183 187 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
184 188 end
185 189
186 190 def notified_project_ids=(ids)
187 191 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
188 192 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
189 193 @notified_projects_ids = nil
190 194 notified_projects_ids
191 195 end
192 196
193 197 def self.find_by_rss_key(key)
194 198 token = Token.find_by_value(key)
195 199 token && token.user.active? ? token.user : nil
196 200 end
197 201
198 202 def self.find_by_autologin_key(key)
199 203 token = Token.find_by_action_and_value('autologin', key)
200 204 token && (token.created_on > Setting.autologin.to_i.day.ago) && token.user.active? ? token.user : nil
201 205 end
202 206
203 207 # Makes find_by_mail case-insensitive
204 208 def self.find_by_mail(mail)
205 209 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
206 210 end
207 211
208 212 # Sort users by their display names
209 213 def <=>(user)
210 214 self.to_s.downcase <=> user.to_s.downcase
211 215 end
212 216
213 217 def to_s
214 218 name
215 219 end
216 220
217 221 def logged?
218 222 true
219 223 end
220 224
221 225 def anonymous?
222 226 !logged?
223 227 end
224 228
225 229 # Return user's role for project
226 230 def role_for_project(project)
227 231 # No role on archived projects
228 232 return nil unless project && project.active?
229 233 if logged?
230 234 # Find project membership
231 235 membership = memberships.detect {|m| m.project_id == project.id}
232 236 if membership
233 237 membership.role
234 238 else
235 239 @role_non_member ||= Role.non_member
236 240 end
237 241 else
238 242 @role_anonymous ||= Role.anonymous
239 243 end
240 244 end
241 245
242 246 # Return true if the user is a member of project
243 247 def member_of?(project)
244 248 role_for_project(project).member?
245 249 end
246 250
247 251 # Return true if the user is allowed to do the specified action on project
248 252 # action can be:
249 253 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
250 254 # * a permission Symbol (eg. :edit_project)
251 255 def allowed_to?(action, project, options={})
252 256 if project
253 257 # No action allowed on archived projects
254 258 return false unless project.active?
255 259 # No action allowed on disabled modules
256 260 return false unless project.allows_to?(action)
257 261 # Admin users are authorized for anything else
258 262 return true if admin?
259 263
260 264 role = role_for_project(project)
261 265 return false unless role
262 266 role.allowed_to?(action) && (project.is_public? || role.member?)
263 267
264 268 elsif options[:global]
265 269 # authorize if user has at least one role that has this permission
266 270 roles = memberships.collect {|m| m.role}.uniq
267 271 roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
268 272 else
269 273 false
270 274 end
271 275 end
272 276
273 277 def self.current=(user)
274 278 @current_user = user
275 279 end
276 280
277 281 def self.current
278 282 @current_user ||= User.anonymous
279 283 end
280 284
281 285 def self.anonymous
282 286 anonymous_user = AnonymousUser.find(:first)
283 287 if anonymous_user.nil?
284 288 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
285 289 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
286 290 end
287 291 anonymous_user
288 292 end
289 293
290 294 private
291 295 # Return password digest
292 296 def self.hash_password(clear_password)
293 297 Digest::SHA1.hexdigest(clear_password || "")
294 298 end
295 299 end
296 300
297 301 class AnonymousUser < User
298 302
299 303 def validate_on_create
300 304 # There should be only one AnonymousUser in the database
301 305 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
302 306 end
303 307
304 308 def available_custom_fields
305 309 []
306 310 end
307 311
308 312 # Overrides a few properties
309 313 def logged?; false end
310 314 def admin; false end
311 315 def name; 'Anonymous' end
312 316 def mail; nil end
313 317 def time_zone; nil end
314 318 def rss_key; nil end
315 319 end
@@ -1,212 +1,223
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_mail_uniqueness_should_not_be_case_sensitive
53 53 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
54 54 u.login = 'newuser1'
55 55 u.password, u.password_confirmation = "password", "password"
56 56 assert u.save
57 57
58 58 u = User.new(:firstname => "new", :lastname => "user", :mail => "newUser@Somenet.foo")
59 59 u.login = 'newuser2'
60 60 u.password, u.password_confirmation = "password", "password"
61 61 assert !u.save
62 62 assert_equal 'activerecord_error_taken', u.errors.on(:mail)
63 63 end
64 64
65 65 def test_update
66 66 assert_equal "admin", @admin.login
67 67 @admin.login = "john"
68 68 assert @admin.save, @admin.errors.full_messages.join("; ")
69 69 @admin.reload
70 70 assert_equal "john", @admin.login
71 71 end
72 72
73 73 def test_destroy
74 74 User.find(2).destroy
75 75 assert_nil User.find_by_id(2)
76 76 assert Member.find_all_by_user_id(2).empty?
77 77 end
78 78
79 79 def test_validate
80 80 @admin.login = ""
81 81 assert !@admin.save
82 82 assert_equal 1, @admin.errors.count
83 83 end
84 84
85 85 def test_password
86 86 user = User.try_to_login("admin", "admin")
87 87 assert_kind_of User, user
88 88 assert_equal "admin", user.login
89 89 user.password = "hello"
90 90 assert user.save
91 91
92 92 user = User.try_to_login("admin", "hello")
93 93 assert_kind_of User, user
94 94 assert_equal "admin", user.login
95 95 assert_equal User.hash_password("hello"), user.hashed_password
96 96 end
97 97
98 98 def test_name_format
99 99 assert_equal 'Smith, John', @jsmith.name(:lastname_coma_firstname)
100 100 Setting.user_format = :firstname_lastname
101 101 assert_equal 'John Smith', @jsmith.reload.name
102 102 Setting.user_format = :username
103 103 assert_equal 'jsmith', @jsmith.reload.name
104 104 end
105 105
106 106 def test_lock
107 107 user = User.try_to_login("jsmith", "jsmith")
108 108 assert_equal @jsmith, user
109 109
110 110 @jsmith.status = User::STATUS_LOCKED
111 111 assert @jsmith.save
112 112
113 113 user = User.try_to_login("jsmith", "jsmith")
114 114 assert_equal nil, user
115 115 end
116 116
117 117 def test_create_anonymous
118 118 AnonymousUser.delete_all
119 119 anon = User.anonymous
120 120 assert !anon.new_record?
121 121 assert_kind_of AnonymousUser, anon
122 122 end
123 123
124 124 def test_rss_key
125 125 assert_nil @jsmith.rss_token
126 126 key = @jsmith.rss_key
127 127 assert_equal 40, key.length
128 128
129 129 @jsmith.reload
130 130 assert_equal key, @jsmith.rss_key
131 131 end
132 132
133 133 def test_role_for_project
134 134 # user with a role
135 135 role = @jsmith.role_for_project(Project.find(1))
136 136 assert_kind_of Role, role
137 137 assert_equal "Manager", role.name
138 138
139 139 # user with no role
140 140 assert !@dlopper.role_for_project(Project.find(2)).member?
141 141 end
142 142
143 143 def test_mail_notification_all
144 144 @jsmith.mail_notification = true
145 145 @jsmith.notified_project_ids = []
146 146 @jsmith.save
147 147 @jsmith.reload
148 148 assert @jsmith.projects.first.recipients.include?(@jsmith.mail)
149 149 end
150 150
151 151 def test_mail_notification_selected
152 152 @jsmith.mail_notification = false
153 153 @jsmith.notified_project_ids = [1]
154 154 @jsmith.save
155 155 @jsmith.reload
156 156 assert Project.find(1).recipients.include?(@jsmith.mail)
157 157 end
158 158
159 159 def test_mail_notification_none
160 160 @jsmith.mail_notification = false
161 161 @jsmith.notified_project_ids = []
162 162 @jsmith.save
163 163 @jsmith.reload
164 164 assert !@jsmith.projects.first.recipients.include?(@jsmith.mail)
165 165 end
166 166
167 167 def test_comments_sorting_preference
168 168 assert !@jsmith.wants_comments_in_reverse_order?
169 169 @jsmith.pref.comments_sorting = 'asc'
170 170 assert !@jsmith.wants_comments_in_reverse_order?
171 171 @jsmith.pref.comments_sorting = 'desc'
172 172 assert @jsmith.wants_comments_in_reverse_order?
173 173 end
174 174
175 175 def test_find_by_mail_should_be_case_insensitive
176 176 u = User.find_by_mail('JSmith@somenet.foo')
177 177 assert_not_nil u
178 178 assert_equal 'jsmith@somenet.foo', u.mail
179 179 end
180 180
181 181 def test_random_password
182 182 u = User.new
183 183 u.random_password
184 184 assert !u.password.blank?
185 185 assert !u.password_confirmation.blank?
186 186 end
187 187
188 188 if Object.const_defined?(:OpenID)
189 189
190 190 def test_setting_identity_url
191 191 normalized_open_id_url = 'http://example.com/'
192 192 u = User.new( :identity_url => 'http://example.com/' )
193 193 assert_equal normalized_open_id_url, u.identity_url
194 194 end
195 195
196 196 def test_setting_identity_url_without_trailing_slash
197 197 normalized_open_id_url = 'http://example.com/'
198 198 u = User.new( :identity_url => 'http://example.com' )
199 199 assert_equal normalized_open_id_url, u.identity_url
200 200 end
201 201
202 202 def test_setting_identity_url_without_protocol
203 203 normalized_open_id_url = 'http://example.com/'
204 204 u = User.new( :identity_url => 'example.com' )
205 205 assert_equal normalized_open_id_url, u.identity_url
206 206 end
207
208 def test_setting_blank_identity_url
209 u = User.new( :identity_url => 'example.com' )
210 u.identity_url = ''
211 assert u.identity_url.blank?
212 end
213
214 def test_setting_invalid_identity_url
215 u = User.new( :identity_url => 'this is not an openid url' )
216 assert u.identity_url.blank?
217 end
207 218
208 219 else
209 220 puts "Skipping openid tests."
210 221 end
211 222
212 223 end
General Comments 0
You need to be logged in to leave comments. Login now