##// END OF EJS Templates
Adds random salt to user passwords (#7410)....
Jean-Philippe Lang -
r4816:ce84bb1a0194
parent child
Show More
@@ -0,0 +1,9
1 class AddUsersSalt < ActiveRecord::Migration
2 def self.up
3 add_column :users, :salt, :string, :limit => 64
4 end
5
6 def self.down
7 remove_column :users, :salt
8 end
9 end
@@ -0,0 +1,13
1 class SaltUserPasswords < ActiveRecord::Migration
2
3 def self.up
4 say_with_time "Salting user passwords, this may take some time..." do
5 User.salt_unsalted_passwords!
6 end
7 end
8
9 def self.down
10 # Unsalted passwords can not be restored
11 raise ActiveRecord::IrreversibleMigration, "Can't decypher salted passwords. This migration can not be rollback'ed."
12 end
13 end
@@ -1,542 +1,572
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 include Redmine::SafeAttributes
22 22
23 23 # Account statuses
24 24 STATUS_ANONYMOUS = 0
25 25 STATUS_ACTIVE = 1
26 26 STATUS_REGISTERED = 2
27 27 STATUS_LOCKED = 3
28 28
29 29 USER_FORMATS = {
30 30 :firstname_lastname => '#{firstname} #{lastname}',
31 31 :firstname => '#{firstname}',
32 32 :lastname_firstname => '#{lastname} #{firstname}',
33 33 :lastname_coma_firstname => '#{lastname}, #{firstname}',
34 34 :username => '#{login}'
35 35 }
36 36
37 37 MAIL_NOTIFICATION_OPTIONS = [
38 38 ['all', :label_user_mail_option_all],
39 39 ['selected', :label_user_mail_option_selected],
40 40 ['only_my_events', :label_user_mail_option_only_my_events],
41 41 ['only_assigned', :label_user_mail_option_only_assigned],
42 42 ['only_owner', :label_user_mail_option_only_owner],
43 43 ['none', :label_user_mail_option_none]
44 44 ]
45 45
46 46 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
47 47 :after_remove => Proc.new {|user, group| group.user_removed(user)}
48 48 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
49 49 has_many :changesets, :dependent => :nullify
50 50 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
51 51 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
52 52 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
53 53 belongs_to :auth_source
54 54
55 55 # Active non-anonymous users scope
56 56 named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
57 57
58 58 acts_as_customizable
59 59
60 60 attr_accessor :password, :password_confirmation
61 61 attr_accessor :last_before_login_on
62 62 # Prevents unauthorized assignments
63 63 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
64 64
65 65 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
66 66 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }, :case_sensitive => false
67 67 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
68 68 # Login must contain lettres, numbers, underscores only
69 69 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
70 70 validates_length_of :login, :maximum => 30
71 71 validates_length_of :firstname, :lastname, :maximum => 30
72 72 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
73 73 validates_length_of :mail, :maximum => 60, :allow_nil => true
74 74 validates_confirmation_of :password, :allow_nil => true
75 75 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
76 76
77 77 before_destroy :remove_references_before_destroy
78 78
79 79 def before_create
80 80 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
81 81 true
82 82 end
83 83
84 84 def before_save
85 85 # update hashed_password if password was set
86 self.hashed_password = User.hash_password(self.password) if self.password && self.auth_source_id.blank?
86 if self.password && self.auth_source_id.blank?
87 salt_password(password)
88 end
87 89 end
88 90
89 91 def reload(*args)
90 92 @name = nil
91 93 super
92 94 end
93 95
94 96 def mail=(arg)
95 97 write_attribute(:mail, arg.to_s.strip)
96 98 end
97 99
98 100 def identity_url=(url)
99 101 if url.blank?
100 102 write_attribute(:identity_url, '')
101 103 else
102 104 begin
103 105 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
104 106 rescue OpenIdAuthentication::InvalidOpenId
105 107 # Invlaid url, don't save
106 108 end
107 109 end
108 110 self.read_attribute(:identity_url)
109 111 end
110 112
111 113 # Returns the user that matches provided login and password, or nil
112 114 def self.try_to_login(login, password)
113 115 # Make sure no one can sign in with an empty password
114 116 return nil if password.to_s.empty?
115 117 user = find_by_login(login)
116 118 if user
117 119 # user is already in local database
118 120 return nil if !user.active?
119 121 if user.auth_source
120 122 # user has an external authentication method
121 123 return nil unless user.auth_source.authenticate(login, password)
122 124 else
123 125 # authentication with local password
124 return nil unless User.hash_password(password) == user.hashed_password
126 return nil unless user.check_password?(password)
125 127 end
126 128 else
127 129 # user is not yet registered, try to authenticate with available sources
128 130 attrs = AuthSource.authenticate(login, password)
129 131 if attrs
130 132 user = new(attrs)
131 133 user.login = login
132 134 user.language = Setting.default_language
133 135 if user.save
134 136 user.reload
135 137 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
136 138 end
137 139 end
138 140 end
139 141 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
140 142 user
141 143 rescue => text
142 144 raise text
143 145 end
144 146
145 147 # Returns the user who matches the given autologin +key+ or nil
146 148 def self.try_to_autologin(key)
147 149 tokens = Token.find_all_by_action_and_value('autologin', key)
148 150 # Make sure there's only 1 token that matches the key
149 151 if tokens.size == 1
150 152 token = tokens.first
151 153 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
152 154 token.user.update_attribute(:last_login_on, Time.now)
153 155 token.user
154 156 end
155 157 end
156 158 end
157 159
158 160 # Return user's full name for display
159 161 def name(formatter = nil)
160 162 if formatter
161 163 eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
162 164 else
163 165 @name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
164 166 end
165 167 end
166 168
167 169 def active?
168 170 self.status == STATUS_ACTIVE
169 171 end
170 172
171 173 def registered?
172 174 self.status == STATUS_REGISTERED
173 175 end
174 176
175 177 def locked?
176 178 self.status == STATUS_LOCKED
177 179 end
178 180
179 181 def activate
180 182 self.status = STATUS_ACTIVE
181 183 end
182 184
183 185 def register
184 186 self.status = STATUS_REGISTERED
185 187 end
186 188
187 189 def lock
188 190 self.status = STATUS_LOCKED
189 191 end
190 192
191 193 def activate!
192 194 update_attribute(:status, STATUS_ACTIVE)
193 195 end
194 196
195 197 def register!
196 198 update_attribute(:status, STATUS_REGISTERED)
197 199 end
198 200
199 201 def lock!
200 202 update_attribute(:status, STATUS_LOCKED)
201 203 end
202 204
205 # Returns true if +clear_password+ is the correct user's password, otherwise false
203 206 def check_password?(clear_password)
204 207 if auth_source_id.present?
205 208 auth_source.authenticate(self.login, clear_password)
206 209 else
207 User.hash_password(clear_password) == self.hashed_password
210 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
208 211 end
209 212 end
213
214 # Generates a random salt and computes hashed_password for +clear_password+
215 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
216 def salt_password(clear_password)
217 self.salt = User.generate_salt
218 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
219 end
210 220
211 221 # Does the backend storage allow this user to change their password?
212 222 def change_password_allowed?
213 223 return true if auth_source_id.blank?
214 224 return auth_source.allow_password_changes?
215 225 end
216 226
217 227 # Generate and set a random password. Useful for automated user creation
218 228 # Based on Token#generate_token_value
219 229 #
220 230 def random_password
221 231 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
222 232 password = ''
223 233 40.times { |i| password << chars[rand(chars.size-1)] }
224 234 self.password = password
225 235 self.password_confirmation = password
226 236 self
227 237 end
228 238
229 239 def pref
230 240 self.preference ||= UserPreference.new(:user => self)
231 241 end
232 242
233 243 def time_zone
234 244 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
235 245 end
236 246
237 247 def wants_comments_in_reverse_order?
238 248 self.pref[:comments_sorting] == 'desc'
239 249 end
240 250
241 251 # Return user's RSS key (a 40 chars long string), used to access feeds
242 252 def rss_key
243 253 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
244 254 token.value
245 255 end
246 256
247 257 # Return user's API key (a 40 chars long string), used to access the API
248 258 def api_key
249 259 token = self.api_token || self.create_api_token(:action => 'api')
250 260 token.value
251 261 end
252 262
253 263 # Return an array of project ids for which the user has explicitly turned mail notifications on
254 264 def notified_projects_ids
255 265 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
256 266 end
257 267
258 268 def notified_project_ids=(ids)
259 269 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
260 270 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
261 271 @notified_projects_ids = nil
262 272 notified_projects_ids
263 273 end
264 274
265 275 def valid_notification_options
266 276 self.class.valid_notification_options(self)
267 277 end
268 278
269 279 # Only users that belong to more than 1 project can select projects for which they are notified
270 280 def self.valid_notification_options(user=nil)
271 281 # Note that @user.membership.size would fail since AR ignores
272 282 # :include association option when doing a count
273 283 if user.nil? || user.memberships.length < 1
274 284 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
275 285 else
276 286 MAIL_NOTIFICATION_OPTIONS
277 287 end
278 288 end
279 289
280 290 # Find a user account by matching the exact login and then a case-insensitive
281 291 # version. Exact matches will be given priority.
282 292 def self.find_by_login(login)
283 293 # force string comparison to be case sensitive on MySQL
284 294 type_cast = (ActiveRecord::Base.connection.adapter_name == 'MySQL') ? 'BINARY' : ''
285 295
286 296 # First look for an exact match
287 297 user = first(:conditions => ["#{type_cast} login = ?", login])
288 298 # Fail over to case-insensitive if none was found
289 299 user ||= first(:conditions => ["#{type_cast} LOWER(login) = ?", login.to_s.downcase])
290 300 end
291 301
292 302 def self.find_by_rss_key(key)
293 303 token = Token.find_by_value(key)
294 304 token && token.user.active? ? token.user : nil
295 305 end
296 306
297 307 def self.find_by_api_key(key)
298 308 token = Token.find_by_action_and_value('api', key)
299 309 token && token.user.active? ? token.user : nil
300 310 end
301 311
302 312 # Makes find_by_mail case-insensitive
303 313 def self.find_by_mail(mail)
304 314 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
305 315 end
306 316
307 317 def to_s
308 318 name
309 319 end
310 320
311 321 # Returns the current day according to user's time zone
312 322 def today
313 323 if time_zone.nil?
314 324 Date.today
315 325 else
316 326 Time.now.in_time_zone(time_zone).to_date
317 327 end
318 328 end
319 329
320 330 def logged?
321 331 true
322 332 end
323 333
324 334 def anonymous?
325 335 !logged?
326 336 end
327 337
328 338 # Return user's roles for project
329 339 def roles_for_project(project)
330 340 roles = []
331 341 # No role on archived projects
332 342 return roles unless project && project.active?
333 343 if logged?
334 344 # Find project membership
335 345 membership = memberships.detect {|m| m.project_id == project.id}
336 346 if membership
337 347 roles = membership.roles
338 348 else
339 349 @role_non_member ||= Role.non_member
340 350 roles << @role_non_member
341 351 end
342 352 else
343 353 @role_anonymous ||= Role.anonymous
344 354 roles << @role_anonymous
345 355 end
346 356 roles
347 357 end
348 358
349 359 # Return true if the user is a member of project
350 360 def member_of?(project)
351 361 !roles_for_project(project).detect {|role| role.member?}.nil?
352 362 end
353 363
354 364 # Return true if the user is allowed to do the specified action on a specific context
355 365 # Action can be:
356 366 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
357 367 # * a permission Symbol (eg. :edit_project)
358 368 # Context can be:
359 369 # * a project : returns true if user is allowed to do the specified action on this project
360 370 # * a group of projects : returns true if user is allowed on every project
361 371 # * nil with options[:global] set : check if user has at least one role allowed for this action,
362 372 # or falls back to Non Member / Anonymous permissions depending if the user is logged
363 373 def allowed_to?(action, context, options={})
364 374 if context && context.is_a?(Project)
365 375 # No action allowed on archived projects
366 376 return false unless context.active?
367 377 # No action allowed on disabled modules
368 378 return false unless context.allows_to?(action)
369 379 # Admin users are authorized for anything else
370 380 return true if admin?
371 381
372 382 roles = roles_for_project(context)
373 383 return false unless roles
374 384 roles.detect {|role| (context.is_public? || role.member?) && role.allowed_to?(action)}
375 385
376 386 elsif context && context.is_a?(Array)
377 387 # Authorize if user is authorized on every element of the array
378 388 context.map do |project|
379 389 allowed_to?(action,project,options)
380 390 end.inject do |memo,allowed|
381 391 memo && allowed
382 392 end
383 393 elsif options[:global]
384 394 # Admin users are always authorized
385 395 return true if admin?
386 396
387 397 # authorize if user has at least one role that has this permission
388 398 roles = memberships.collect {|m| m.roles}.flatten.uniq
389 399 roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
390 400 else
391 401 false
392 402 end
393 403 end
394 404
395 405 # Is the user allowed to do the specified action on any project?
396 406 # See allowed_to? for the actions and valid options.
397 407 def allowed_to_globally?(action, options)
398 408 allowed_to?(action, nil, options.reverse_merge(:global => true))
399 409 end
400 410
401 411 safe_attributes 'login',
402 412 'firstname',
403 413 'lastname',
404 414 'mail',
405 415 'mail_notification',
406 416 'language',
407 417 'custom_field_values',
408 418 'custom_fields',
409 419 'identity_url'
410 420
411 421 safe_attributes 'status',
412 422 'auth_source_id',
413 423 :if => lambda {|user, current_user| current_user.admin?}
414 424
415 425 safe_attributes 'group_ids',
416 426 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
417 427
418 428 # Utility method to help check if a user should be notified about an
419 429 # event.
420 430 #
421 431 # TODO: only supports Issue events currently
422 432 def notify_about?(object)
423 433 case mail_notification
424 434 when 'all'
425 435 true
426 436 when 'selected'
427 437 # user receives notifications for created/assigned issues on unselected projects
428 438 if object.is_a?(Issue) && (object.author == self || object.assigned_to == self)
429 439 true
430 440 else
431 441 false
432 442 end
433 443 when 'none'
434 444 false
435 445 when 'only_my_events'
436 446 if object.is_a?(Issue) && (object.author == self || object.assigned_to == self)
437 447 true
438 448 else
439 449 false
440 450 end
441 451 when 'only_assigned'
442 452 if object.is_a?(Issue) && object.assigned_to == self
443 453 true
444 454 else
445 455 false
446 456 end
447 457 when 'only_owner'
448 458 if object.is_a?(Issue) && object.author == self
449 459 true
450 460 else
451 461 false
452 462 end
453 463 else
454 464 false
455 465 end
456 466 end
457 467
458 468 def self.current=(user)
459 469 @current_user = user
460 470 end
461 471
462 472 def self.current
463 473 @current_user ||= User.anonymous
464 474 end
465 475
466 476 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
467 477 # one anonymous user per database.
468 478 def self.anonymous
469 479 anonymous_user = AnonymousUser.find(:first)
470 480 if anonymous_user.nil?
471 481 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
472 482 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
473 483 end
474 484 anonymous_user
475 485 end
486
487 # Salts all existing unsalted passwords
488 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
489 # This method is used in the SaltPasswords migration and is to be kept as is
490 def self.salt_unsalted_passwords!
491 transaction do
492 User.find_each(:conditions => "salt IS NULL OR salt = ''") do |user|
493 next if user.hashed_password.blank?
494 salt = User.generate_salt
495 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
496 User.update_all("salt = '#{salt}', hashed_password = '#{hashed_password}'", ["id = ?", user.id] )
497 end
498 end
499 end
476 500
477 501 protected
478 502
479 503 def validate
480 504 # Password length validation based on setting
481 505 if !password.nil? && password.size < Setting.password_min_length.to_i
482 506 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
483 507 end
484 508 end
485 509
486 510 private
487 511
488 512 # Removes references that are not handled by associations
489 513 # Things that are not deleted are reassociated with the anonymous user
490 514 def remove_references_before_destroy
491 515 return if self.id.nil?
492 516
493 517 substitute = User.anonymous
494 518 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
495 519 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
496 520 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
497 521 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
498 522 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
499 523 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
500 524 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
501 525 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
502 526 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
503 527 # Remove private queries and keep public ones
504 528 Query.delete_all ['user_id = ? AND is_public = ?', id, false]
505 529 Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
506 530 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
507 531 Token.delete_all ['user_id = ?', id]
508 532 Watcher.delete_all ['user_id = ?', id]
509 533 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
510 534 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
511 535 end
512 536
513 537 # Return password digest
514 538 def self.hash_password(clear_password)
515 539 Digest::SHA1.hexdigest(clear_password || "")
516 540 end
541
542 # Returns a 128bits random salt as a hex string (32 chars long)
543 def self.generate_salt
544 ActiveSupport::SecureRandom.hex(16)
545 end
546
517 547 end
518 548
519 549 class AnonymousUser < User
520 550
521 551 def validate_on_create
522 552 # There should be only one AnonymousUser in the database
523 553 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
524 554 end
525 555
526 556 def available_custom_fields
527 557 []
528 558 end
529 559
530 560 # Overrides a few properties
531 561 def logged?; false end
532 562 def admin; false end
533 563 def name(*args); I18n.t(:label_user_anonymous) end
534 564 def mail; nil end
535 565 def time_zone; nil end
536 566 def rss_key; nil end
537 567
538 568 # Anonymous user can not be destroyed
539 569 def destroy
540 570 false
541 571 end
542 572 end
@@ -1,386 +1,387
1 1 package Apache::Authn::Redmine;
2 2
3 3 =head1 Apache::Authn::Redmine
4 4
5 5 Redmine - a mod_perl module to authenticate webdav subversion users
6 6 against redmine database
7 7
8 8 =head1 SYNOPSIS
9 9
10 10 This module allow anonymous users to browse public project and
11 11 registred users to browse and commit their project. Authentication is
12 12 done against the redmine database or the LDAP configured in redmine.
13 13
14 14 This method is far simpler than the one with pam_* and works with all
15 15 database without an hassle but you need to have apache/mod_perl on the
16 16 svn server.
17 17
18 18 =head1 INSTALLATION
19 19
20 20 For this to automagically work, you need to have a recent reposman.rb
21 21 (after r860) and if you already use reposman, read the last section to
22 22 migrate.
23 23
24 24 Sorry ruby users but you need some perl modules, at least mod_perl2,
25 25 DBI and DBD::mysql (or the DBD driver for you database as it should
26 26 work on allmost all databases).
27 27
28 28 On debian/ubuntu you must do :
29 29
30 30 aptitude install libapache-dbi-perl libapache2-mod-perl2 libdbd-mysql-perl
31 31
32 32 If your Redmine users use LDAP authentication, you will also need
33 33 Authen::Simple::LDAP (and IO::Socket::SSL if LDAPS is used):
34 34
35 35 aptitude install libauthen-simple-ldap-perl libio-socket-ssl-perl
36 36
37 37 =head1 CONFIGURATION
38 38
39 39 ## This module has to be in your perl path
40 40 ## eg: /usr/lib/perl5/Apache/Authn/Redmine.pm
41 41 PerlLoadModule Apache::Authn::Redmine
42 42 <Location /svn>
43 43 DAV svn
44 44 SVNParentPath "/var/svn"
45 45
46 46 AuthType Basic
47 47 AuthName redmine
48 48 Require valid-user
49 49
50 50 PerlAccessHandler Apache::Authn::Redmine::access_handler
51 51 PerlAuthenHandler Apache::Authn::Redmine::authen_handler
52 52
53 53 ## for mysql
54 54 RedmineDSN "DBI:mysql:database=databasename;host=my.db.server"
55 55 ## for postgres
56 56 # RedmineDSN "DBI:Pg:dbname=databasename;host=my.db.server"
57 57
58 58 RedmineDbUser "redmine"
59 59 RedmineDbPass "password"
60 60 ## Optional where clause (fulltext search would be slow and
61 61 ## database dependant).
62 62 # RedmineDbWhereClause "and members.role_id IN (1,2)"
63 63 ## Optional credentials cache size
64 64 # RedmineCacheCredsMax 50
65 65 </Location>
66 66
67 67 To be able to browse repository inside redmine, you must add something
68 68 like that :
69 69
70 70 <Location /svn-private>
71 71 DAV svn
72 72 SVNParentPath "/var/svn"
73 73 Order deny,allow
74 74 Deny from all
75 75 # only allow reading orders
76 76 <Limit GET PROPFIND OPTIONS REPORT>
77 77 Allow from redmine.server.ip
78 78 </Limit>
79 79 </Location>
80 80
81 81 and you will have to use this reposman.rb command line to create repository :
82 82
83 83 reposman.rb --redmine my.redmine.server --svn-dir /var/svn --owner www-data -u http://svn.server/svn-private/
84 84
85 85 =head1 MIGRATION FROM OLDER RELEASES
86 86
87 87 If you use an older reposman.rb (r860 or before), you need to change
88 88 rights on repositories to allow the apache user to read and write
89 89 S<them :>
90 90
91 91 sudo chown -R www-data /var/svn/*
92 92 sudo chmod -R u+w /var/svn/*
93 93
94 94 And you need to upgrade at least reposman.rb (after r860).
95 95
96 96 =cut
97 97
98 98 use strict;
99 99 use warnings FATAL => 'all', NONFATAL => 'redefine';
100 100
101 101 use DBI;
102 102 use Digest::SHA1;
103 103 # optional module for LDAP authentication
104 104 my $CanUseLDAPAuth = eval("use Authen::Simple::LDAP; 1");
105 105
106 106 use Apache2::Module;
107 107 use Apache2::Access;
108 108 use Apache2::ServerRec qw();
109 109 use Apache2::RequestRec qw();
110 110 use Apache2::RequestUtil qw();
111 111 use Apache2::Const qw(:common :override :cmd_how);
112 112 use APR::Pool ();
113 113 use APR::Table ();
114 114
115 115 # use Apache2::Directive qw();
116 116
117 117 my @directives = (
118 118 {
119 119 name => 'RedmineDSN',
120 120 req_override => OR_AUTHCFG,
121 121 args_how => TAKE1,
122 122 errmsg => 'Dsn in format used by Perl DBI. eg: "DBI:Pg:dbname=databasename;host=my.db.server"',
123 123 },
124 124 {
125 125 name => 'RedmineDbUser',
126 126 req_override => OR_AUTHCFG,
127 127 args_how => TAKE1,
128 128 },
129 129 {
130 130 name => 'RedmineDbPass',
131 131 req_override => OR_AUTHCFG,
132 132 args_how => TAKE1,
133 133 },
134 134 {
135 135 name => 'RedmineDbWhereClause',
136 136 req_override => OR_AUTHCFG,
137 137 args_how => TAKE1,
138 138 },
139 139 {
140 140 name => 'RedmineCacheCredsMax',
141 141 req_override => OR_AUTHCFG,
142 142 args_how => TAKE1,
143 143 errmsg => 'RedmineCacheCredsMax must be decimal number',
144 144 },
145 145 );
146 146
147 147 sub RedmineDSN {
148 148 my ($self, $parms, $arg) = @_;
149 149 $self->{RedmineDSN} = $arg;
150 150 my $query = "SELECT
151 hashed_password, auth_source_id, permissions
151 hashed_password, salt, auth_source_id, permissions
152 152 FROM members, projects, users, roles, member_roles
153 153 WHERE
154 154 projects.id=members.project_id
155 155 AND member_roles.member_id=members.id
156 156 AND users.id=members.user_id
157 157 AND roles.id=member_roles.role_id
158 158 AND users.status=1
159 159 AND login=?
160 160 AND identifier=? ";
161 161 $self->{RedmineQuery} = trim($query);
162 162 }
163 163
164 164 sub RedmineDbUser { set_val('RedmineDbUser', @_); }
165 165 sub RedmineDbPass { set_val('RedmineDbPass', @_); }
166 166 sub RedmineDbWhereClause {
167 167 my ($self, $parms, $arg) = @_;
168 168 $self->{RedmineQuery} = trim($self->{RedmineQuery}.($arg ? $arg : "")." ");
169 169 }
170 170
171 171 sub RedmineCacheCredsMax {
172 172 my ($self, $parms, $arg) = @_;
173 173 if ($arg) {
174 174 $self->{RedmineCachePool} = APR::Pool->new;
175 175 $self->{RedmineCacheCreds} = APR::Table::make($self->{RedmineCachePool}, $arg);
176 176 $self->{RedmineCacheCredsCount} = 0;
177 177 $self->{RedmineCacheCredsMax} = $arg;
178 178 }
179 179 }
180 180
181 181 sub trim {
182 182 my $string = shift;
183 183 $string =~ s/\s{2,}/ /g;
184 184 return $string;
185 185 }
186 186
187 187 sub set_val {
188 188 my ($key, $self, $parms, $arg) = @_;
189 189 $self->{$key} = $arg;
190 190 }
191 191
192 192 Apache2::Module::add(__PACKAGE__, \@directives);
193 193
194 194
195 195 my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/;
196 196
197 197 sub access_handler {
198 198 my $r = shift;
199 199
200 200 unless ($r->some_auth_required) {
201 201 $r->log_reason("No authentication has been configured");
202 202 return FORBIDDEN;
203 203 }
204 204
205 205 my $method = $r->method;
206 206 return OK unless defined $read_only_methods{$method};
207 207
208 208 my $project_id = get_project_identifier($r);
209 209
210 210 $r->set_handlers(PerlAuthenHandler => [\&OK])
211 211 if is_public_project($project_id, $r);
212 212
213 213 return OK
214 214 }
215 215
216 216 sub authen_handler {
217 217 my $r = shift;
218 218
219 219 my ($res, $redmine_pass) = $r->get_basic_auth_pw();
220 220 return $res unless $res == OK;
221 221
222 222 if (is_member($r->user, $redmine_pass, $r)) {
223 223 return OK;
224 224 } else {
225 225 $r->note_auth_failure();
226 226 return AUTH_REQUIRED;
227 227 }
228 228 }
229 229
230 230 # check if authentication is forced
231 231 sub is_authentication_forced {
232 232 my $r = shift;
233 233
234 234 my $dbh = connect_database($r);
235 235 my $sth = $dbh->prepare(
236 236 "SELECT value FROM settings where settings.name = 'login_required';"
237 237 );
238 238
239 239 $sth->execute();
240 240 my $ret = 0;
241 241 if (my @row = $sth->fetchrow_array) {
242 242 if ($row[0] eq "1" || $row[0] eq "t") {
243 243 $ret = 1;
244 244 }
245 245 }
246 246 $sth->finish();
247 247 undef $sth;
248 248
249 249 $dbh->disconnect();
250 250 undef $dbh;
251 251
252 252 $ret;
253 253 }
254 254
255 255 sub is_public_project {
256 256 my $project_id = shift;
257 257 my $r = shift;
258 258
259 259 if (is_authentication_forced($r)) {
260 260 return 0;
261 261 }
262 262
263 263 my $dbh = connect_database($r);
264 264 my $sth = $dbh->prepare(
265 265 "SELECT is_public FROM projects WHERE projects.identifier = ?;"
266 266 );
267 267
268 268 $sth->execute($project_id);
269 269 my $ret = 0;
270 270 if (my @row = $sth->fetchrow_array) {
271 271 if ($row[0] eq "1" || $row[0] eq "t") {
272 272 $ret = 1;
273 273 }
274 274 }
275 275 $sth->finish();
276 276 undef $sth;
277 277 $dbh->disconnect();
278 278 undef $dbh;
279 279
280 280 $ret;
281 281 }
282 282
283 283 # perhaps we should use repository right (other read right) to check public access.
284 284 # it could be faster BUT it doesn't work for the moment.
285 285 # sub is_public_project_by_file {
286 286 # my $project_id = shift;
287 287 # my $r = shift;
288 288
289 289 # my $tree = Apache2::Directive::conftree();
290 290 # my $node = $tree->lookup('Location', $r->location);
291 291 # my $hash = $node->as_hash;
292 292
293 293 # my $svnparentpath = $hash->{SVNParentPath};
294 294 # my $repos_path = $svnparentpath . "/" . $project_id;
295 295 # return 1 if (stat($repos_path))[2] & 00007;
296 296 # }
297 297
298 298 sub is_member {
299 299 my $redmine_user = shift;
300 300 my $redmine_pass = shift;
301 301 my $r = shift;
302 302
303 303 my $dbh = connect_database($r);
304 304 my $project_id = get_project_identifier($r);
305 305
306 306 my $pass_digest = Digest::SHA1::sha1_hex($redmine_pass);
307 307
308 308 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
309 309 my $usrprojpass;
310 310 if ($cfg->{RedmineCacheCredsMax}) {
311 311 $usrprojpass = $cfg->{RedmineCacheCreds}->get($redmine_user.":".$project_id);
312 312 return 1 if (defined $usrprojpass and ($usrprojpass eq $pass_digest));
313 313 }
314 314 my $query = $cfg->{RedmineQuery};
315 315 my $sth = $dbh->prepare($query);
316 316 $sth->execute($redmine_user, $project_id);
317 317
318 318 my $ret;
319 while (my ($hashed_password, $auth_source_id, $permissions) = $sth->fetchrow_array) {
319 while (my ($hashed_password, $salt, $auth_source_id, $permissions) = $sth->fetchrow_array) {
320 320
321 321 unless ($auth_source_id) {
322 my $method = $r->method;
323 if ($hashed_password eq $pass_digest && ((defined $read_only_methods{$method} && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/) ) {
322 my $method = $r->method;
323 my $salted_password = Digest::SHA1::sha1_hex($salt.$pass_digest);
324 if ($hashed_password eq $salted_password && ((defined $read_only_methods{$method} && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/) ) {
324 325 $ret = 1;
325 326 last;
326 327 }
327 328 } elsif ($CanUseLDAPAuth) {
328 329 my $sthldap = $dbh->prepare(
329 330 "SELECT host,port,tls,account,account_password,base_dn,attr_login from auth_sources WHERE id = ?;"
330 331 );
331 332 $sthldap->execute($auth_source_id);
332 333 while (my @rowldap = $sthldap->fetchrow_array) {
333 334 my $ldap = Authen::Simple::LDAP->new(
334 335 host => ($rowldap[2] eq "1" || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]:$rowldap[1]" : $rowldap[0],
335 336 port => $rowldap[1],
336 337 basedn => $rowldap[5],
337 338 binddn => $rowldap[3] ? $rowldap[3] : "",
338 339 bindpw => $rowldap[4] ? $rowldap[4] : "",
339 340 filter => "(".$rowldap[6]."=%s)"
340 341 );
341 342 my $method = $r->method;
342 343 $ret = 1 if ($ldap->authenticate($redmine_user, $redmine_pass) && ((defined $read_only_methods{$method} && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/));
343 344
344 345 }
345 346 $sthldap->finish();
346 347 undef $sthldap;
347 348 }
348 349 }
349 350 $sth->finish();
350 351 undef $sth;
351 352 $dbh->disconnect();
352 353 undef $dbh;
353 354
354 355 if ($cfg->{RedmineCacheCredsMax} and $ret) {
355 356 if (defined $usrprojpass) {
356 357 $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id, $pass_digest);
357 358 } else {
358 359 if ($cfg->{RedmineCacheCredsCount} < $cfg->{RedmineCacheCredsMax}) {
359 360 $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id, $pass_digest);
360 361 $cfg->{RedmineCacheCredsCount}++;
361 362 } else {
362 363 $cfg->{RedmineCacheCreds}->clear();
363 364 $cfg->{RedmineCacheCredsCount} = 0;
364 365 }
365 366 }
366 367 }
367 368
368 369 $ret;
369 370 }
370 371
371 372 sub get_project_identifier {
372 373 my $r = shift;
373 374
374 375 my $location = $r->location;
375 376 my ($identifier) = $r->uri =~ m{$location/*([^/]+)};
376 377 $identifier;
377 378 }
378 379
379 380 sub connect_database {
380 381 my $r = shift;
381 382
382 383 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
383 384 return DBI->connect($cfg->{RedmineDSN}, $cfg->{RedmineDbUser}, $cfg->{RedmineDbPass});
384 385 }
385 386
386 387 1;
@@ -1,156 +1,164
1 1 ---
2 2 users_004:
3 3 created_on: 2006-07-19 19:34:07 +02:00
4 4 status: 1
5 5 last_login_on:
6 6 language: en
7 hashed_password: 4e4aeb7baaf0706bd670263fef42dad15763b608
7 # password = foo
8 salt: 3126f764c3c5ac61cbfc103f25f934cf
9 hashed_password: 9e4dd7eeb172c12a0691a6d9d3a269f7e9fe671b
8 10 updated_on: 2006-07-19 19:34:07 +02:00
9 11 admin: false
10 12 mail: rhill@somenet.foo
11 13 lastname: Hill
12 14 firstname: Robert
13 15 id: 4
14 16 auth_source_id:
15 17 mail_notification: all
16 18 login: rhill
17 19 type: User
18 20 users_001:
19 21 created_on: 2006-07-19 19:12:21 +02:00
20 22 status: 1
21 23 last_login_on: 2006-07-19 22:57:52 +02:00
22 24 language: en
23 hashed_password: d033e22ae348aeb5660fc2140aec35850c4da997
25 # password = admin
26 salt: 82090c953c4a0000a7db253b0691a6b4
27 hashed_password: b5b6ff9543bf1387374cdfa27a54c96d236a7150
24 28 updated_on: 2006-07-19 22:57:52 +02:00
25 29 admin: true
26 30 mail: admin@somenet.foo
27 31 lastname: Admin
28 32 firstname: redMine
29 33 id: 1
30 34 auth_source_id:
31 35 mail_notification: all
32 36 login: admin
33 37 type: User
34 38 users_002:
35 39 created_on: 2006-07-19 19:32:09 +02:00
36 40 status: 1
37 41 last_login_on: 2006-07-19 22:42:15 +02:00
38 42 language: en
39 hashed_password: a9a653d4151fa2c081ba1ffc2c2726f3b80b7d7d
43 # password = jsmith
44 salt: 67eb4732624d5a7753dcea7ce0bb7d7d
45 hashed_password: bfbe06043353a677d0215b26a5800d128d5413bc
40 46 updated_on: 2006-07-19 22:42:15 +02:00
41 47 admin: false
42 48 mail: jsmith@somenet.foo
43 49 lastname: Smith
44 50 firstname: John
45 51 id: 2
46 52 auth_source_id:
47 53 mail_notification: all
48 54 login: jsmith
49 55 type: User
50 56 users_003:
51 57 created_on: 2006-07-19 19:33:19 +02:00
52 58 status: 1
53 59 last_login_on:
54 60 language: en
55 hashed_password: 7feb7657aa7a7bf5aef3414a5084875f27192415
61 # password = foo
62 salt: 7599f9963ec07b5a3b55b354407120c0
63 hashed_password: 8f659c8d7c072f189374edacfa90d6abbc26d8ed
56 64 updated_on: 2006-07-19 19:33:19 +02:00
57 65 admin: false
58 66 mail: dlopper@somenet.foo
59 67 lastname: Lopper
60 68 firstname: Dave
61 69 id: 3
62 70 auth_source_id:
63 71 mail_notification: all
64 72 login: dlopper
65 73 type: User
66 74 users_005:
67 75 id: 5
68 76 created_on: 2006-07-19 19:33:19 +02:00
69 77 # Locked
70 78 status: 3
71 79 last_login_on:
72 80 language: en
73 hashed_password: 7feb7657aa7a7bf5aef3414a5084875f27192415
81 hashed_password: 1
74 82 updated_on: 2006-07-19 19:33:19 +02:00
75 83 admin: false
76 84 mail: dlopper2@somenet.foo
77 85 lastname: Lopper2
78 86 firstname: Dave2
79 87 auth_source_id:
80 88 mail_notification: all
81 89 login: dlopper2
82 90 type: User
83 91 users_006:
84 92 id: 6
85 93 created_on: 2006-07-19 19:33:19 +02:00
86 94 status: 0
87 95 last_login_on:
88 96 language: ''
89 97 hashed_password: 1
90 98 updated_on: 2006-07-19 19:33:19 +02:00
91 99 admin: false
92 100 mail: ''
93 101 lastname: Anonymous
94 102 firstname: ''
95 103 auth_source_id:
96 104 mail_notification: only_my_events
97 105 login: ''
98 106 type: AnonymousUser
99 107 users_007:
100 108 id: 7
101 109 created_on: 2006-07-19 19:33:19 +02:00
102 110 status: 1
103 111 last_login_on:
104 112 language: ''
105 113 hashed_password: 1
106 114 updated_on: 2006-07-19 19:33:19 +02:00
107 115 admin: false
108 116 mail: someone@foo.bar
109 117 lastname: One
110 118 firstname: Some
111 119 auth_source_id:
112 120 mail_notification: only_my_events
113 121 login: someone
114 122 type: User
115 123 users_008:
116 124 id: 8
117 125 created_on: 2006-07-19 19:33:19 +02:00
118 126 status: 1
119 127 last_login_on:
120 128 language: 'it'
121 129 hashed_password: 1
122 130 updated_on: 2006-07-19 19:33:19 +02:00
123 131 admin: false
124 132 mail: miscuser8@foo.bar
125 133 lastname: Misc
126 134 firstname: User
127 135 auth_source_id:
128 136 mail_notification: only_my_events
129 137 login: miscuser8
130 138 type: User
131 139 users_009:
132 140 id: 9
133 141 created_on: 2006-07-19 19:33:19 +02:00
134 142 status: 1
135 143 last_login_on:
136 144 language: 'it'
137 145 hashed_password: 1
138 146 updated_on: 2006-07-19 19:33:19 +02:00
139 147 admin: false
140 148 mail: miscuser9@foo.bar
141 149 lastname: Misc
142 150 firstname: User
143 151 auth_source_id:
144 152 mail_notification: only_my_events
145 153 login: miscuser9
146 154 type: User
147 155 groups_010:
148 156 id: 10
149 157 lastname: A Team
150 158 type: Group
151 159 groups_011:
152 160 id: 11
153 161 lastname: B Team
154 162 type: Group
155 163
156 164
@@ -1,766 +1,798
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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.expand_path('../../test_helper', __FILE__)
19 19
20 20 class UserTest < ActiveSupport::TestCase
21 21 fixtures :users, :members, :projects, :roles, :member_roles, :auth_sources
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 test 'object_daddy creation' do
30 30 User.generate_with_protected!(:firstname => 'Testing connection')
31 31 User.generate_with_protected!(:firstname => 'Testing connection')
32 32 assert_equal 2, User.count(:all, :conditions => {:firstname => 'Testing connection'})
33 33 end
34 34
35 35 def test_truth
36 36 assert_kind_of User, @jsmith
37 37 end
38 38
39 39 def test_mail_should_be_stripped
40 40 u = User.new
41 41 u.mail = " foo@bar.com "
42 42 assert_equal "foo@bar.com", u.mail
43 43 end
44 44
45 45 def test_create
46 46 user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
47 47
48 48 user.login = "jsmith"
49 49 user.password, user.password_confirmation = "password", "password"
50 50 # login uniqueness
51 51 assert !user.save
52 52 assert_equal 1, user.errors.count
53 53
54 54 user.login = "newuser"
55 55 user.password, user.password_confirmation = "passwd", "password"
56 56 # password confirmation
57 57 assert !user.save
58 58 assert_equal 1, user.errors.count
59 59
60 60 user.password, user.password_confirmation = "password", "password"
61 61 assert user.save
62 62 end
63 63
64 64 context "User#before_create" do
65 65 should "set the mail_notification to the default Setting" do
66 66 @user1 = User.generate_with_protected!
67 67 assert_equal 'only_my_events', @user1.mail_notification
68 68
69 69 with_settings :default_notification_option => 'all' do
70 70 @user2 = User.generate_with_protected!
71 71 assert_equal 'all', @user2.mail_notification
72 72 end
73 73 end
74 74 end
75 75
76 76 context "User.login" do
77 77 should "be case-insensitive." do
78 78 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
79 79 u.login = 'newuser'
80 80 u.password, u.password_confirmation = "password", "password"
81 81 assert u.save
82 82
83 83 u = User.new(:firstname => "Similar", :lastname => "User", :mail => "similaruser@somenet.foo")
84 84 u.login = 'NewUser'
85 85 u.password, u.password_confirmation = "password", "password"
86 86 assert !u.save
87 87 assert_equal I18n.translate('activerecord.errors.messages.taken'), u.errors.on(:login)
88 88 end
89 89 end
90 90
91 91 def test_mail_uniqueness_should_not_be_case_sensitive
92 92 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
93 93 u.login = 'newuser1'
94 94 u.password, u.password_confirmation = "password", "password"
95 95 assert u.save
96 96
97 97 u = User.new(:firstname => "new", :lastname => "user", :mail => "newUser@Somenet.foo")
98 98 u.login = 'newuser2'
99 99 u.password, u.password_confirmation = "password", "password"
100 100 assert !u.save
101 101 assert_equal I18n.translate('activerecord.errors.messages.taken'), u.errors.on(:mail)
102 102 end
103 103
104 104 def test_update
105 105 assert_equal "admin", @admin.login
106 106 @admin.login = "john"
107 107 assert @admin.save, @admin.errors.full_messages.join("; ")
108 108 @admin.reload
109 109 assert_equal "john", @admin.login
110 110 end
111 111
112 112 def test_destroy_should_delete_members_and_roles
113 113 members = Member.find_all_by_user_id(2)
114 114 ms = members.size
115 115 rs = members.collect(&:roles).flatten.size
116 116
117 117 assert_difference 'Member.count', - ms do
118 118 assert_difference 'MemberRole.count', - rs do
119 119 User.find(2).destroy
120 120 end
121 121 end
122 122
123 123 assert_nil User.find_by_id(2)
124 124 assert Member.find_all_by_user_id(2).empty?
125 125 end
126 126
127 127 def test_destroy_should_update_attachments
128 128 attachment = Attachment.create!(:container => Project.find(1),
129 129 :file => uploaded_test_file("testfile.txt", "text/plain"),
130 130 :author_id => 2)
131 131
132 132 User.find(2).destroy
133 133 assert_nil User.find_by_id(2)
134 134 assert_equal User.anonymous, attachment.reload.author
135 135 end
136 136
137 137 def test_destroy_should_update_comments
138 138 comment = Comment.create!(
139 139 :commented => News.create!(:project_id => 1, :author_id => 1, :title => 'foo', :description => 'foo'),
140 140 :author => User.find(2),
141 141 :comments => 'foo'
142 142 )
143 143
144 144 User.find(2).destroy
145 145 assert_nil User.find_by_id(2)
146 146 assert_equal User.anonymous, comment.reload.author
147 147 end
148 148
149 149 def test_destroy_should_update_issues
150 150 issue = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'foo')
151 151
152 152 User.find(2).destroy
153 153 assert_nil User.find_by_id(2)
154 154 assert_equal User.anonymous, issue.reload.author
155 155 end
156 156
157 157 def test_destroy_should_unassign_issues
158 158 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2)
159 159
160 160 User.find(2).destroy
161 161 assert_nil User.find_by_id(2)
162 162 assert_nil issue.reload.assigned_to
163 163 end
164 164
165 165 def test_destroy_should_update_journals
166 166 issue = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'foo')
167 167 issue.init_journal(User.find(2), "update")
168 168 issue.save!
169 169
170 170 User.find(2).destroy
171 171 assert_nil User.find_by_id(2)
172 172 assert_equal User.anonymous, issue.journals.first.reload.user
173 173 end
174 174
175 175 def test_destroy_should_update_journal_details_old_value
176 176 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2)
177 177 issue.init_journal(User.find(1), "update")
178 178 issue.assigned_to_id = nil
179 179 assert_difference 'JournalDetail.count' do
180 180 issue.save!
181 181 end
182 182 journal_detail = JournalDetail.first(:order => 'id DESC')
183 183 assert_equal '2', journal_detail.old_value
184 184
185 185 User.find(2).destroy
186 186 assert_nil User.find_by_id(2)
187 187 assert_equal User.anonymous.id.to_s, journal_detail.reload.old_value
188 188 end
189 189
190 190 def test_destroy_should_update_journal_details_value
191 191 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo')
192 192 issue.init_journal(User.find(1), "update")
193 193 issue.assigned_to_id = 2
194 194 assert_difference 'JournalDetail.count' do
195 195 issue.save!
196 196 end
197 197 journal_detail = JournalDetail.first(:order => 'id DESC')
198 198 assert_equal '2', journal_detail.value
199 199
200 200 User.find(2).destroy
201 201 assert_nil User.find_by_id(2)
202 202 assert_equal User.anonymous.id.to_s, journal_detail.reload.value
203 203 end
204 204
205 205 def test_destroy_should_update_messages
206 206 board = Board.create!(:project_id => 1, :name => 'Board', :description => 'Board')
207 207 message = Message.create!(:board_id => board.id, :author_id => 2, :subject => 'foo', :content => 'foo')
208 208
209 209 User.find(2).destroy
210 210 assert_nil User.find_by_id(2)
211 211 assert_equal User.anonymous, message.reload.author
212 212 end
213 213
214 214 def test_destroy_should_update_news
215 215 news = News.create!(:project_id => 1, :author_id => 2, :title => 'foo', :description => 'foo')
216 216
217 217 User.find(2).destroy
218 218 assert_nil User.find_by_id(2)
219 219 assert_equal User.anonymous, news.reload.author
220 220 end
221 221
222 222 def test_destroy_should_delete_private_queries
223 223 query = Query.new(:name => 'foo', :is_public => false)
224 224 query.project_id = 1
225 225 query.user_id = 2
226 226 query.save!
227 227
228 228 User.find(2).destroy
229 229 assert_nil User.find_by_id(2)
230 230 assert_nil Query.find_by_id(query.id)
231 231 end
232 232
233 233 def test_destroy_should_update_public_queries
234 234 query = Query.new(:name => 'foo', :is_public => true)
235 235 query.project_id = 1
236 236 query.user_id = 2
237 237 query.save!
238 238
239 239 User.find(2).destroy
240 240 assert_nil User.find_by_id(2)
241 241 assert_equal User.anonymous, query.reload.user
242 242 end
243 243
244 244 def test_destroy_should_update_time_entries
245 245 entry = TimeEntry.new(:hours => '2', :spent_on => Date.today, :activity => TimeEntryActivity.create!(:name => 'foo'))
246 246 entry.project_id = 1
247 247 entry.user_id = 2
248 248 entry.save!
249 249
250 250 User.find(2).destroy
251 251 assert_nil User.find_by_id(2)
252 252 assert_equal User.anonymous, entry.reload.user
253 253 end
254 254
255 255 def test_destroy_should_delete_tokens
256 256 token = Token.create!(:user_id => 2, :value => 'foo')
257 257
258 258 User.find(2).destroy
259 259 assert_nil User.find_by_id(2)
260 260 assert_nil Token.find_by_id(token.id)
261 261 end
262 262
263 263 def test_destroy_should_delete_watchers
264 264 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo')
265 265 watcher = Watcher.create!(:user_id => 2, :watchable => issue)
266 266
267 267 User.find(2).destroy
268 268 assert_nil User.find_by_id(2)
269 269 assert_nil Watcher.find_by_id(watcher.id)
270 270 end
271 271
272 272 def test_destroy_should_update_wiki_contents
273 273 wiki_content = WikiContent.create!(
274 274 :text => 'foo',
275 275 :author_id => 2,
276 276 :page => WikiPage.create!(:title => 'Foo', :wiki => Wiki.create!(:project_id => 1, :start_page => 'Start'))
277 277 )
278 278 wiki_content.text = 'bar'
279 279 assert_difference 'WikiContent::Version.count' do
280 280 wiki_content.save!
281 281 end
282 282
283 283 User.find(2).destroy
284 284 assert_nil User.find_by_id(2)
285 285 assert_equal User.anonymous, wiki_content.reload.author
286 286 wiki_content.versions.each do |version|
287 287 assert_equal User.anonymous, version.reload.author
288 288 end
289 289 end
290 290
291 291 def test_destroy_should_nullify_issue_categories
292 292 category = IssueCategory.create!(:project_id => 1, :assigned_to_id => 2, :name => 'foo')
293 293
294 294 User.find(2).destroy
295 295 assert_nil User.find_by_id(2)
296 296 assert_nil category.reload.assigned_to_id
297 297 end
298 298
299 299 def test_destroy_should_nullify_changesets
300 300 changeset = Changeset.create!(
301 301 :repository => Repository::Subversion.create!(
302 302 :project_id => 1,
303 303 :url => 'file:///var/svn'
304 304 ),
305 305 :revision => '12',
306 306 :committed_on => Time.now,
307 307 :committer => 'jsmith'
308 308 )
309 309 assert_equal 2, changeset.user_id
310 310
311 311 User.find(2).destroy
312 312 assert_nil User.find_by_id(2)
313 313 assert_nil changeset.reload.user_id
314 314 end
315 315
316 316 def test_anonymous_user_should_not_be_destroyable
317 317 assert_no_difference 'User.count' do
318 318 assert_equal false, User.anonymous.destroy
319 319 end
320 320 end
321 321
322 322 def test_validate_login_presence
323 323 @admin.login = ""
324 324 assert !@admin.save
325 325 assert_equal 1, @admin.errors.count
326 326 end
327 327
328 328 def test_validate_mail_notification_inclusion
329 329 u = User.new
330 330 u.mail_notification = 'foo'
331 331 u.save
332 332 assert_not_nil u.errors.on(:mail_notification)
333 333 end
334 334
335 335 context "User#try_to_login" do
336 336 should "fall-back to case-insensitive if user login is not found as-typed." do
337 337 user = User.try_to_login("AdMin", "admin")
338 338 assert_kind_of User, user
339 339 assert_equal "admin", user.login
340 340 end
341 341
342 342 should "select the exact matching user first" do
343 343 case_sensitive_user = User.generate_with_protected!(:login => 'changed', :password => 'admin', :password_confirmation => 'admin')
344 344 # bypass validations to make it appear like existing data
345 345 case_sensitive_user.update_attribute(:login, 'ADMIN')
346 346
347 347 user = User.try_to_login("ADMIN", "admin")
348 348 assert_kind_of User, user
349 349 assert_equal "ADMIN", user.login
350 350
351 351 end
352 352 end
353 353
354 354 def test_password
355 355 user = User.try_to_login("admin", "admin")
356 356 assert_kind_of User, user
357 357 assert_equal "admin", user.login
358 358 user.password = "hello"
359 359 assert user.save
360 360
361 361 user = User.try_to_login("admin", "hello")
362 362 assert_kind_of User, user
363 363 assert_equal "admin", user.login
364 assert_equal User.hash_password("hello"), user.hashed_password
365 364 end
366 365
367 366 def test_name_format
368 367 assert_equal 'Smith, John', @jsmith.name(:lastname_coma_firstname)
369 368 Setting.user_format = :firstname_lastname
370 369 assert_equal 'John Smith', @jsmith.reload.name
371 370 Setting.user_format = :username
372 371 assert_equal 'jsmith', @jsmith.reload.name
373 372 end
374 373
375 374 def test_lock
376 375 user = User.try_to_login("jsmith", "jsmith")
377 376 assert_equal @jsmith, user
378 377
379 378 @jsmith.status = User::STATUS_LOCKED
380 379 assert @jsmith.save
381 380
382 381 user = User.try_to_login("jsmith", "jsmith")
383 382 assert_equal nil, user
384 383 end
385 384
385 context ".try_to_login" do
386 context "with good credentials" do
387 should "return the user" do
388 user = User.try_to_login("admin", "admin")
389 assert_kind_of User, user
390 assert_equal "admin", user.login
391 end
392 end
393
394 context "with wrong credentials" do
395 should "return nil" do
396 assert_nil User.try_to_login("admin", "foo")
397 end
398 end
399 end
400
386 401 if ldap_configured?
387 402 context "#try_to_login using LDAP" do
388 403 context "with failed connection to the LDAP server" do
389 404 should "return nil" do
390 405 @auth_source = AuthSourceLdap.find(1)
391 406 AuthSource.any_instance.stubs(:initialize_ldap_con).raises(Net::LDAP::LdapError, 'Cannot connect')
392 407
393 408 assert_equal nil, User.try_to_login('edavis', 'wrong')
394 409 end
395 410 end
396 411
397 412 context "with an unsuccessful authentication" do
398 413 should "return nil" do
399 414 assert_equal nil, User.try_to_login('edavis', 'wrong')
400 415 end
401 416 end
402 417
403 418 context "on the fly registration" do
404 419 setup do
405 420 @auth_source = AuthSourceLdap.find(1)
406 421 end
407 422
408 423 context "with a successful authentication" do
409 424 should "create a new user account if it doesn't exist" do
410 425 assert_difference('User.count') do
411 426 user = User.try_to_login('edavis', '123456')
412 427 assert !user.admin?
413 428 end
414 429 end
415 430
416 431 should "retrieve existing user" do
417 432 user = User.try_to_login('edavis', '123456')
418 433 user.admin = true
419 434 user.save!
420 435
421 436 assert_no_difference('User.count') do
422 437 user = User.try_to_login('edavis', '123456')
423 438 assert user.admin?
424 439 end
425 440 end
426 441 end
427 442 end
428 443 end
429 444
430 445 else
431 446 puts "Skipping LDAP tests."
432 447 end
433 448
434 449 def test_create_anonymous
435 450 AnonymousUser.delete_all
436 451 anon = User.anonymous
437 452 assert !anon.new_record?
438 453 assert_kind_of AnonymousUser, anon
439 454 end
440 455
441 456 should_have_one :rss_token
442 457
443 458 def test_rss_key
444 459 assert_nil @jsmith.rss_token
445 460 key = @jsmith.rss_key
446 461 assert_equal 40, key.length
447 462
448 463 @jsmith.reload
449 464 assert_equal key, @jsmith.rss_key
450 465 end
451 466
452 467
453 468 should_have_one :api_token
454 469
455 470 context "User#api_key" do
456 471 should "generate a new one if the user doesn't have one" do
457 472 user = User.generate_with_protected!(:api_token => nil)
458 473 assert_nil user.api_token
459 474
460 475 key = user.api_key
461 476 assert_equal 40, key.length
462 477 user.reload
463 478 assert_equal key, user.api_key
464 479 end
465 480
466 481 should "return the existing api token value" do
467 482 user = User.generate_with_protected!
468 483 token = Token.generate!(:action => 'api')
469 484 user.api_token = token
470 485 assert user.save
471 486
472 487 assert_equal token.value, user.api_key
473 488 end
474 489 end
475 490
476 491 context "User#find_by_api_key" do
477 492 should "return nil if no matching key is found" do
478 493 assert_nil User.find_by_api_key('zzzzzzzzz')
479 494 end
480 495
481 496 should "return nil if the key is found for an inactive user" do
482 497 user = User.generate_with_protected!(:status => User::STATUS_LOCKED)
483 498 token = Token.generate!(:action => 'api')
484 499 user.api_token = token
485 500 user.save
486 501
487 502 assert_nil User.find_by_api_key(token.value)
488 503 end
489 504
490 505 should "return the user if the key is found for an active user" do
491 506 user = User.generate_with_protected!(:status => User::STATUS_ACTIVE)
492 507 token = Token.generate!(:action => 'api')
493 508 user.api_token = token
494 509 user.save
495 510
496 511 assert_equal user, User.find_by_api_key(token.value)
497 512 end
498 513 end
499 514
500 515 def test_roles_for_project
501 516 # user with a role
502 517 roles = @jsmith.roles_for_project(Project.find(1))
503 518 assert_kind_of Role, roles.first
504 519 assert_equal "Manager", roles.first.name
505 520
506 521 # user with no role
507 522 assert_nil @dlopper.roles_for_project(Project.find(2)).detect {|role| role.member?}
508 523 end
509 524
510 525 def test_valid_notification_options
511 526 # without memberships
512 527 assert_equal 5, User.find(7).valid_notification_options.size
513 528 # with memberships
514 529 assert_equal 6, User.find(2).valid_notification_options.size
515 530 end
516 531
517 532 def test_valid_notification_options_class_method
518 533 assert_equal 5, User.valid_notification_options.size
519 534 assert_equal 5, User.valid_notification_options(User.find(7)).size
520 535 assert_equal 6, User.valid_notification_options(User.find(2)).size
521 536 end
522 537
523 538 def test_mail_notification_all
524 539 @jsmith.mail_notification = 'all'
525 540 @jsmith.notified_project_ids = []
526 541 @jsmith.save
527 542 @jsmith.reload
528 543 assert @jsmith.projects.first.recipients.include?(@jsmith.mail)
529 544 end
530 545
531 546 def test_mail_notification_selected
532 547 @jsmith.mail_notification = 'selected'
533 548 @jsmith.notified_project_ids = [1]
534 549 @jsmith.save
535 550 @jsmith.reload
536 551 assert Project.find(1).recipients.include?(@jsmith.mail)
537 552 end
538 553
539 554 def test_mail_notification_only_my_events
540 555 @jsmith.mail_notification = 'only_my_events'
541 556 @jsmith.notified_project_ids = []
542 557 @jsmith.save
543 558 @jsmith.reload
544 559 assert !@jsmith.projects.first.recipients.include?(@jsmith.mail)
545 560 end
546 561
547 562 def test_comments_sorting_preference
548 563 assert !@jsmith.wants_comments_in_reverse_order?
549 564 @jsmith.pref.comments_sorting = 'asc'
550 565 assert !@jsmith.wants_comments_in_reverse_order?
551 566 @jsmith.pref.comments_sorting = 'desc'
552 567 assert @jsmith.wants_comments_in_reverse_order?
553 568 end
554 569
555 570 def test_find_by_mail_should_be_case_insensitive
556 571 u = User.find_by_mail('JSmith@somenet.foo')
557 572 assert_not_nil u
558 573 assert_equal 'jsmith@somenet.foo', u.mail
559 574 end
560 575
561 576 def test_random_password
562 577 u = User.new
563 578 u.random_password
564 579 assert !u.password.blank?
565 580 assert !u.password_confirmation.blank?
566 581 end
567 582
568 583 context "#change_password_allowed?" do
569 584 should "be allowed if no auth source is set" do
570 585 user = User.generate_with_protected!
571 586 assert user.change_password_allowed?
572 587 end
573 588
574 589 should "delegate to the auth source" do
575 590 user = User.generate_with_protected!
576 591
577 592 allowed_auth_source = AuthSource.generate!
578 593 def allowed_auth_source.allow_password_changes?; true; end
579 594
580 595 denied_auth_source = AuthSource.generate!
581 596 def denied_auth_source.allow_password_changes?; false; end
582 597
583 598 assert user.change_password_allowed?
584 599
585 600 user.auth_source = allowed_auth_source
586 601 assert user.change_password_allowed?, "User not allowed to change password, though auth source does"
587 602
588 603 user.auth_source = denied_auth_source
589 604 assert !user.change_password_allowed?, "User allowed to change password, though auth source does not"
590 605 end
591 606
592 607 end
593 608
594 609 context "#allowed_to?" do
595 610 context "with a unique project" do
596 611 should "return false if project is archived" do
597 612 project = Project.find(1)
598 613 Project.any_instance.stubs(:status).returns(Project::STATUS_ARCHIVED)
599 614 assert ! @admin.allowed_to?(:view_issues, Project.find(1))
600 615 end
601 616
602 617 should "return false if related module is disabled" do
603 618 project = Project.find(1)
604 619 project.enabled_module_names = ["issue_tracking"]
605 620 assert @admin.allowed_to?(:add_issues, project)
606 621 assert ! @admin.allowed_to?(:view_wiki_pages, project)
607 622 end
608 623
609 624 should "authorize nearly everything for admin users" do
610 625 project = Project.find(1)
611 626 assert ! @admin.member_of?(project)
612 627 %w(edit_issues delete_issues manage_news manage_documents manage_wiki).each do |p|
613 628 assert @admin.allowed_to?(p.to_sym, project)
614 629 end
615 630 end
616 631
617 632 should "authorize normal users depending on their roles" do
618 633 project = Project.find(1)
619 634 assert @jsmith.allowed_to?(:delete_messages, project) #Manager
620 635 assert ! @dlopper.allowed_to?(:delete_messages, project) #Developper
621 636 end
622 637 end
623 638
624 639 context "with multiple projects" do
625 640 should "return false if array is empty" do
626 641 assert ! @admin.allowed_to?(:view_project, [])
627 642 end
628 643
629 644 should "return true only if user has permission on all these projects" do
630 645 assert @admin.allowed_to?(:view_project, Project.all)
631 646 assert ! @dlopper.allowed_to?(:view_project, Project.all) #cannot see Project(2)
632 647 assert @jsmith.allowed_to?(:edit_issues, @jsmith.projects) #Manager or Developer everywhere
633 648 assert ! @jsmith.allowed_to?(:delete_issue_watchers, @jsmith.projects) #Dev cannot delete_issue_watchers
634 649 end
635 650
636 651 should "behave correctly with arrays of 1 project" do
637 652 assert ! User.anonymous.allowed_to?(:delete_issues, [Project.first])
638 653 end
639 654 end
640 655
641 656 context "with options[:global]" do
642 657 should "authorize if user has at least one role that has this permission" do
643 658 @dlopper2 = User.find(5) #only Developper on a project, not Manager anywhere
644 659 @anonymous = User.find(6)
645 660 assert @jsmith.allowed_to?(:delete_issue_watchers, nil, :global => true)
646 661 assert ! @dlopper2.allowed_to?(:delete_issue_watchers, nil, :global => true)
647 662 assert @dlopper2.allowed_to?(:add_issues, nil, :global => true)
648 663 assert ! @anonymous.allowed_to?(:add_issues, nil, :global => true)
649 664 assert @anonymous.allowed_to?(:view_issues, nil, :global => true)
650 665 end
651 666 end
652 667 end
653 668
654 669 context "User#notify_about?" do
655 670 context "Issues" do
656 671 setup do
657 672 @project = Project.find(1)
658 673 @author = User.generate_with_protected!
659 674 @assignee = User.generate_with_protected!
660 675 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
661 676 end
662 677
663 678 should "be true for a user with :all" do
664 679 @author.update_attribute(:mail_notification, 'all')
665 680 assert @author.notify_about?(@issue)
666 681 end
667 682
668 683 should "be false for a user with :none" do
669 684 @author.update_attribute(:mail_notification, 'none')
670 685 assert ! @author.notify_about?(@issue)
671 686 end
672 687
673 688 should "be false for a user with :only_my_events and isn't an author, creator, or assignee" do
674 689 @user = User.generate_with_protected!(:mail_notification => 'only_my_events')
675 690 Member.create!(:user => @user, :project => @project, :role_ids => [1])
676 691 assert ! @user.notify_about?(@issue)
677 692 end
678 693
679 694 should "be true for a user with :only_my_events and is the author" do
680 695 @author.update_attribute(:mail_notification, 'only_my_events')
681 696 assert @author.notify_about?(@issue)
682 697 end
683 698
684 699 should "be true for a user with :only_my_events and is the assignee" do
685 700 @assignee.update_attribute(:mail_notification, 'only_my_events')
686 701 assert @assignee.notify_about?(@issue)
687 702 end
688 703
689 704 should "be true for a user with :only_assigned and is the assignee" do
690 705 @assignee.update_attribute(:mail_notification, 'only_assigned')
691 706 assert @assignee.notify_about?(@issue)
692 707 end
693 708
694 709 should "be false for a user with :only_assigned and is not the assignee" do
695 710 @author.update_attribute(:mail_notification, 'only_assigned')
696 711 assert ! @author.notify_about?(@issue)
697 712 end
698 713
699 714 should "be true for a user with :only_owner and is the author" do
700 715 @author.update_attribute(:mail_notification, 'only_owner')
701 716 assert @author.notify_about?(@issue)
702 717 end
703 718
704 719 should "be false for a user with :only_owner and is not the author" do
705 720 @assignee.update_attribute(:mail_notification, 'only_owner')
706 721 assert ! @assignee.notify_about?(@issue)
707 722 end
708 723
709 724 should "be true for a user with :selected and is the author" do
710 725 @author.update_attribute(:mail_notification, 'selected')
711 726 assert @author.notify_about?(@issue)
712 727 end
713 728
714 729 should "be true for a user with :selected and is the assignee" do
715 730 @assignee.update_attribute(:mail_notification, 'selected')
716 731 assert @assignee.notify_about?(@issue)
717 732 end
718 733
719 734 should "be false for a user with :selected and is not the author or assignee" do
720 735 @user = User.generate_with_protected!(:mail_notification => 'selected')
721 736 Member.create!(:user => @user, :project => @project, :role_ids => [1])
722 737 assert ! @user.notify_about?(@issue)
723 738 end
724 739 end
725 740
726 741 context "other events" do
727 742 should 'be added and tested'
728 743 end
729 744 end
745
746 def test_salt_unsalted_passwords
747 # Restore a user with an unsalted password
748 user = User.find(1)
749 user.salt = nil
750 user.hashed_password = User.hash_password("unsalted")
751 user.save!
752
753 User.salt_unsalted_passwords!
754
755 user.reload
756 # Salt added
757 assert !user.salt.blank?
758 # Password still valid
759 assert user.check_password?("unsalted")
760 assert_equal user, User.try_to_login(user.login, "unsalted")
761 end
730 762
731 763 if Object.const_defined?(:OpenID)
732 764
733 765 def test_setting_identity_url
734 766 normalized_open_id_url = 'http://example.com/'
735 767 u = User.new( :identity_url => 'http://example.com/' )
736 768 assert_equal normalized_open_id_url, u.identity_url
737 769 end
738 770
739 771 def test_setting_identity_url_without_trailing_slash
740 772 normalized_open_id_url = 'http://example.com/'
741 773 u = User.new( :identity_url => 'http://example.com' )
742 774 assert_equal normalized_open_id_url, u.identity_url
743 775 end
744 776
745 777 def test_setting_identity_url_without_protocol
746 778 normalized_open_id_url = 'http://example.com/'
747 779 u = User.new( :identity_url => 'example.com' )
748 780 assert_equal normalized_open_id_url, u.identity_url
749 781 end
750 782
751 783 def test_setting_blank_identity_url
752 784 u = User.new( :identity_url => 'example.com' )
753 785 u.identity_url = ''
754 786 assert u.identity_url.blank?
755 787 end
756 788
757 789 def test_setting_invalid_identity_url
758 790 u = User.new( :identity_url => 'this is not an openid url' )
759 791 assert u.identity_url.blank?
760 792 end
761 793
762 794 else
763 795 puts "Skipping openid tests."
764 796 end
765 797
766 798 end
General Comments 0
You need to be logged in to leave comments. Login now