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