##// END OF EJS Templates
Keep track of valid user sessions (#21058)....
Jean-Philippe Lang -
r14353:4cd22dcc5595
parent child
Show More
@@ -0,0 +1,10
1 class AddTokensUpdatedOn < ActiveRecord::Migration
2 def self.up
3 add_column :tokens, :updated_on, :timestamp
4 Token.update_all("updated_on = created_on")
5 end
6
7 def self.down
8 remove_column :tokens, :updated_on
9 end
10 end
@@ -0,0 +1,97
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.expand_path('../../test_helper', __FILE__)
19
20 class SessionsTest < Redmine::IntegrationTest
21 fixtures :users, :email_addresses, :roles
22
23 def setup
24 Rails.application.config.redmine_verify_sessions = true
25 end
26
27 def teardown
28 Rails.application.config.redmine_verify_sessions = false
29 end
30
31 def test_change_password_kills_sessions
32 log_user('jsmith', 'jsmith')
33
34 jsmith = User.find(2)
35 jsmith.password = "somenewpassword"
36 jsmith.save!
37
38 get '/my/account'
39 assert_response 302
40 assert flash[:error].match(/Your session has expired/)
41 end
42
43 def test_lock_user_kills_sessions
44 log_user('jsmith', 'jsmith')
45
46 jsmith = User.find(2)
47 assert jsmith.lock!
48 assert jsmith.activate!
49
50 get '/my/account'
51 assert_response 302
52 assert flash[:error].match(/Your session has expired/)
53 end
54
55 def test_update_user_does_not_kill_sessions
56 log_user('jsmith', 'jsmith')
57
58 jsmith = User.find(2)
59 jsmith.firstname = 'Robert'
60 jsmith.save!
61
62 get '/my/account'
63 assert_response 200
64 end
65
66 def test_change_password_generates_a_new_token_for_current_session
67 log_user('jsmith', 'jsmith')
68 assert_not_nil token = session[:tk]
69
70 get '/my/password'
71 assert_response 200
72 post '/my/password', :password => 'jsmith',
73 :new_password => 'secret123',
74 :new_password_confirmation => 'secret123'
75 assert_response 302
76 assert_not_equal token, session[:tk]
77
78 get '/my/account'
79 assert_response 200
80 end
81
82 def test_simultaneous_sessions_should_be_valid
83 first = open_session do |session|
84 session.post "/login", :username => 'jsmith', :password => 'jsmith'
85 end
86 other = open_session do |session|
87 session.post "/login", :username => 'jsmith', :password => 'jsmith'
88 end
89
90 first.get '/my/account'
91 assert_equal 200, first.response.response_code
92 first.post '/logout'
93
94 other.get '/my/account'
95 assert_equal 200, other.response.response_code
96 end
97 end
@@ -1,684 +1,660
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 'uri'
19 19 require 'cgi'
20 20
21 21 class Unauthorized < Exception; end
22 22
23 23 class ApplicationController < ActionController::Base
24 24 include Redmine::I18n
25 25 include Redmine::Pagination
26 26 include Redmine::Hook::Helper
27 27 include RoutesHelper
28 28 helper :routes
29 29
30 30 class_attribute :accept_api_auth_actions
31 31 class_attribute :accept_rss_auth_actions
32 32 class_attribute :model_object
33 33
34 34 layout 'base'
35 35
36 36 protect_from_forgery
37 37
38 38 def verify_authenticity_token
39 39 unless api_request?
40 40 super
41 41 end
42 42 end
43 43
44 44 def handle_unverified_request
45 45 unless api_request?
46 46 super
47 47 cookies.delete(autologin_cookie_name)
48 48 self.logged_user = nil
49 49 set_localization
50 50 render_error :status => 422, :message => "Invalid form authenticity token."
51 51 end
52 52 end
53 53
54 before_filter :session_expiration, :user_setup, :force_logout_if_password_changed, :check_if_login_required, :check_password_change, :set_localization
54 before_filter :session_expiration, :user_setup, :check_if_login_required, :check_password_change, :set_localization
55 55
56 56 rescue_from ::Unauthorized, :with => :deny_access
57 57 rescue_from ::ActionView::MissingTemplate, :with => :missing_template
58 58
59 59 include Redmine::Search::Controller
60 60 include Redmine::MenuManager::MenuController
61 61 helper Redmine::MenuManager::MenuHelper
62 62
63 63 include Redmine::SudoMode::Controller
64 64
65 65 def session_expiration
66 if session[:user_id]
66 if session[:user_id] && Rails.application.config.redmine_verify_sessions != false
67 67 if session_expired? && !try_to_autologin
68 68 set_localization(User.active.find_by_id(session[:user_id]))
69 69 self.logged_user = nil
70 70 flash[:error] = l(:error_session_expired)
71 71 require_login
72 else
73 session[:atime] = Time.now.utc.to_i
74 72 end
75 73 end
76 74 end
77 75
78 76 def session_expired?
79 if Setting.session_lifetime?
80 unless session[:ctime] && (Time.now.utc.to_i - session[:ctime].to_i <= Setting.session_lifetime.to_i * 60)
81 return true
82 end
83 end
84 if Setting.session_timeout?
85 unless session[:atime] && (Time.now.utc.to_i - session[:atime].to_i <= Setting.session_timeout.to_i * 60)
86 return true
87 end
88 end
89 false
77 ! User.verify_session_token(session[:user_id], session[:tk])
90 78 end
91 79
92 80 def start_user_session(user)
93 81 session[:user_id] = user.id
94 session[:ctime] = Time.now.utc.to_i
95 session[:atime] = Time.now.utc.to_i
82 session[:tk] = user.generate_session_token
96 83 if user.must_change_password?
97 84 session[:pwd] = '1'
98 85 end
99 86 end
100 87
101 88 def user_setup
102 89 # Check the settings cache for each request
103 90 Setting.check_cache
104 91 # Find the current user
105 92 User.current = find_current_user
106 93 logger.info(" Current user: " + (User.current.logged? ? "#{User.current.login} (id=#{User.current.id})" : "anonymous")) if logger
107 94 end
108 95
109 96 # Returns the current user or nil if no user is logged in
110 97 # and starts a session if needed
111 98 def find_current_user
112 99 user = nil
113 100 unless api_request?
114 101 if session[:user_id]
115 102 # existing session
116 103 user = (User.active.find(session[:user_id]) rescue nil)
117 104 elsif autologin_user = try_to_autologin
118 105 user = autologin_user
119 106 elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth?
120 107 # RSS key authentication does not start a session
121 108 user = User.find_by_rss_key(params[:key])
122 109 end
123 110 end
124 111 if user.nil? && Setting.rest_api_enabled? && accept_api_auth?
125 112 if (key = api_key_from_request)
126 113 # Use API key
127 114 user = User.find_by_api_key(key)
128 115 elsif request.authorization.to_s =~ /\ABasic /i
129 116 # HTTP Basic, either username/password or API key/random
130 117 authenticate_with_http_basic do |username, password|
131 118 user = User.try_to_login(username, password) || User.find_by_api_key(username)
132 119 end
133 120 if user && user.must_change_password?
134 121 render_error :message => 'You must change your password', :status => 403
135 122 return
136 123 end
137 124 end
138 125 # Switch user if requested by an admin user
139 126 if user && user.admin? && (username = api_switch_user_from_request)
140 127 su = User.find_by_login(username)
141 128 if su && su.active?
142 129 logger.info(" User switched by: #{user.login} (id=#{user.id})") if logger
143 130 user = su
144 131 else
145 132 render_error :message => 'Invalid X-Redmine-Switch-User header', :status => 412
146 133 end
147 134 end
148 135 end
149 136 user
150 137 end
151 138
152 def force_logout_if_password_changed
153 passwd_changed_on = User.current.passwd_changed_on || Time.at(0)
154 # Make sure we force logout only for web browser sessions, not API calls
155 # if the password was changed after the session creation.
156 if session[:user_id] && passwd_changed_on.utc.to_i > session[:ctime].to_i
157 reset_session
158 set_localization
159 flash[:error] = l(:error_session_expired)
160 redirect_to signin_url
161 end
162 end
163
164 139 def autologin_cookie_name
165 140 Redmine::Configuration['autologin_cookie_name'].presence || 'autologin'
166 141 end
167 142
168 143 def try_to_autologin
169 144 if cookies[autologin_cookie_name] && Setting.autologin?
170 145 # auto-login feature starts a new session
171 146 user = User.try_to_autologin(cookies[autologin_cookie_name])
172 147 if user
173 148 reset_session
174 149 start_user_session(user)
175 150 end
176 151 user
177 152 end
178 153 end
179 154
180 155 # Sets the logged in user
181 156 def logged_user=(user)
182 157 reset_session
183 158 if user && user.is_a?(User)
184 159 User.current = user
185 160 start_user_session(user)
186 161 else
187 162 User.current = User.anonymous
188 163 end
189 164 end
190 165
191 166 # Logs out current user
192 167 def logout_user
193 168 if User.current.logged?
194 169 cookies.delete(autologin_cookie_name)
195 170 Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin'])
171 Token.delete_all(["user_id = ? AND action = ? AND value = ?", User.current.id, 'session', session[:tk]])
196 172 self.logged_user = nil
197 173 end
198 174 end
199 175
200 176 # check if login is globally required to access the application
201 177 def check_if_login_required
202 178 # no check needed if user is already logged in
203 179 return true if User.current.logged?
204 180 require_login if Setting.login_required?
205 181 end
206 182
207 183 def check_password_change
208 184 if session[:pwd]
209 185 if User.current.must_change_password?
210 186 flash[:error] = l(:error_password_expired)
211 187 redirect_to my_password_path
212 188 else
213 189 session.delete(:pwd)
214 190 end
215 191 end
216 192 end
217 193
218 194 def set_localization(user=User.current)
219 195 lang = nil
220 196 if user && user.logged?
221 197 lang = find_language(user.language)
222 198 end
223 199 if lang.nil? && !Setting.force_default_language_for_anonymous? && request.env['HTTP_ACCEPT_LANGUAGE']
224 200 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
225 201 if !accept_lang.blank?
226 202 accept_lang = accept_lang.downcase
227 203 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
228 204 end
229 205 end
230 206 lang ||= Setting.default_language
231 207 set_language_if_valid(lang)
232 208 end
233 209
234 210 def require_login
235 211 if !User.current.logged?
236 212 # Extract only the basic url parameters on non-GET requests
237 213 if request.get?
238 214 url = url_for(params)
239 215 else
240 216 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
241 217 end
242 218 respond_to do |format|
243 219 format.html {
244 220 if request.xhr?
245 221 head :unauthorized
246 222 else
247 223 redirect_to signin_path(:back_url => url)
248 224 end
249 225 }
250 226 format.any(:atom, :pdf, :csv) {
251 227 redirect_to signin_path(:back_url => url)
252 228 }
253 229 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
254 230 format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
255 231 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
256 232 format.any { head :unauthorized }
257 233 end
258 234 return false
259 235 end
260 236 true
261 237 end
262 238
263 239 def require_admin
264 240 return unless require_login
265 241 if !User.current.admin?
266 242 render_403
267 243 return false
268 244 end
269 245 true
270 246 end
271 247
272 248 def deny_access
273 249 User.current.logged? ? render_403 : require_login
274 250 end
275 251
276 252 # Authorize the user for the requested action
277 253 def authorize(ctrl = params[:controller], action = params[:action], global = false)
278 254 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
279 255 if allowed
280 256 true
281 257 else
282 258 if @project && @project.archived?
283 259 render_403 :message => :notice_not_authorized_archived_project
284 260 else
285 261 deny_access
286 262 end
287 263 end
288 264 end
289 265
290 266 # Authorize the user for the requested action outside a project
291 267 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
292 268 authorize(ctrl, action, global)
293 269 end
294 270
295 271 # Find project of id params[:id]
296 272 def find_project
297 273 @project = Project.find(params[:id])
298 274 rescue ActiveRecord::RecordNotFound
299 275 render_404
300 276 end
301 277
302 278 # Find project of id params[:project_id]
303 279 def find_project_by_project_id
304 280 @project = Project.find(params[:project_id])
305 281 rescue ActiveRecord::RecordNotFound
306 282 render_404
307 283 end
308 284
309 285 # Find a project based on params[:project_id]
310 286 # TODO: some subclasses override this, see about merging their logic
311 287 def find_optional_project
312 288 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
313 289 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
314 290 allowed ? true : deny_access
315 291 rescue ActiveRecord::RecordNotFound
316 292 render_404
317 293 end
318 294
319 295 # Finds and sets @project based on @object.project
320 296 def find_project_from_association
321 297 render_404 unless @object.present?
322 298
323 299 @project = @object.project
324 300 end
325 301
326 302 def find_model_object
327 303 model = self.class.model_object
328 304 if model
329 305 @object = model.find(params[:id])
330 306 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
331 307 end
332 308 rescue ActiveRecord::RecordNotFound
333 309 render_404
334 310 end
335 311
336 312 def self.model_object(model)
337 313 self.model_object = model
338 314 end
339 315
340 316 # Find the issue whose id is the :id parameter
341 317 # Raises a Unauthorized exception if the issue is not visible
342 318 def find_issue
343 319 # Issue.visible.find(...) can not be used to redirect user to the login form
344 320 # if the issue actually exists but requires authentication
345 321 @issue = Issue.find(params[:id])
346 322 raise Unauthorized unless @issue.visible?
347 323 @project = @issue.project
348 324 rescue ActiveRecord::RecordNotFound
349 325 render_404
350 326 end
351 327
352 328 # Find issues with a single :id param or :ids array param
353 329 # Raises a Unauthorized exception if one of the issues is not visible
354 330 def find_issues
355 331 @issues = Issue.
356 332 where(:id => (params[:id] || params[:ids])).
357 333 preload(:project, :status, :tracker, :priority, :author, :assigned_to, :relations_to, {:custom_values => :custom_field}).
358 334 to_a
359 335 raise ActiveRecord::RecordNotFound if @issues.empty?
360 336 raise Unauthorized unless @issues.all?(&:visible?)
361 337 @projects = @issues.collect(&:project).compact.uniq
362 338 @project = @projects.first if @projects.size == 1
363 339 rescue ActiveRecord::RecordNotFound
364 340 render_404
365 341 end
366 342
367 343 def find_attachments
368 344 if (attachments = params[:attachments]).present?
369 345 att = attachments.values.collect do |attachment|
370 346 Attachment.find_by_token( attachment[:token] ) if attachment[:token].present?
371 347 end
372 348 att.compact!
373 349 end
374 350 @attachments = att || []
375 351 end
376 352
377 353 # make sure that the user is a member of the project (or admin) if project is private
378 354 # used as a before_filter for actions that do not require any particular permission on the project
379 355 def check_project_privacy
380 356 if @project && !@project.archived?
381 357 if @project.visible?
382 358 true
383 359 else
384 360 deny_access
385 361 end
386 362 else
387 363 @project = nil
388 364 render_404
389 365 false
390 366 end
391 367 end
392 368
393 369 def back_url
394 370 url = params[:back_url]
395 371 if url.nil? && referer = request.env['HTTP_REFERER']
396 372 url = CGI.unescape(referer.to_s)
397 373 end
398 374 url
399 375 end
400 376
401 377 def redirect_back_or_default(default, options={})
402 378 back_url = params[:back_url].to_s
403 379 if back_url.present? && valid_url = validate_back_url(back_url)
404 380 redirect_to(valid_url)
405 381 return
406 382 elsif options[:referer]
407 383 redirect_to_referer_or default
408 384 return
409 385 end
410 386 redirect_to default
411 387 false
412 388 end
413 389
414 390 # Returns a validated URL string if back_url is a valid url for redirection,
415 391 # otherwise false
416 392 def validate_back_url(back_url)
417 393 if CGI.unescape(back_url).include?('..')
418 394 return false
419 395 end
420 396
421 397 begin
422 398 uri = URI.parse(back_url)
423 399 rescue URI::InvalidURIError
424 400 return false
425 401 end
426 402
427 403 [:scheme, :host, :port].each do |component|
428 404 if uri.send(component).present? && uri.send(component) != request.send(component)
429 405 return false
430 406 end
431 407 uri.send(:"#{component}=", nil)
432 408 end
433 409 # Always ignore basic user:password in the URL
434 410 uri.userinfo = nil
435 411
436 412 path = uri.to_s
437 413 # Ensure that the remaining URL starts with a slash, followed by a
438 414 # non-slash character or the end
439 415 if path !~ %r{\A/([^/]|\z)}
440 416 return false
441 417 end
442 418
443 419 if path.match(%r{/(login|account/register)})
444 420 return false
445 421 end
446 422
447 423 if relative_url_root.present? && !path.starts_with?(relative_url_root)
448 424 return false
449 425 end
450 426
451 427 return path
452 428 end
453 429 private :validate_back_url
454 430
455 431 def valid_back_url?(back_url)
456 432 !!validate_back_url(back_url)
457 433 end
458 434 private :valid_back_url?
459 435
460 436 # Redirects to the request referer if present, redirects to args or call block otherwise.
461 437 def redirect_to_referer_or(*args, &block)
462 438 redirect_to :back
463 439 rescue ::ActionController::RedirectBackError
464 440 if args.any?
465 441 redirect_to *args
466 442 elsif block_given?
467 443 block.call
468 444 else
469 445 raise "#redirect_to_referer_or takes arguments or a block"
470 446 end
471 447 end
472 448
473 449 def render_403(options={})
474 450 @project = nil
475 451 render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
476 452 return false
477 453 end
478 454
479 455 def render_404(options={})
480 456 render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
481 457 return false
482 458 end
483 459
484 460 # Renders an error response
485 461 def render_error(arg)
486 462 arg = {:message => arg} unless arg.is_a?(Hash)
487 463
488 464 @message = arg[:message]
489 465 @message = l(@message) if @message.is_a?(Symbol)
490 466 @status = arg[:status] || 500
491 467
492 468 respond_to do |format|
493 469 format.html {
494 470 render :template => 'common/error', :layout => use_layout, :status => @status
495 471 }
496 472 format.any { head @status }
497 473 end
498 474 end
499 475
500 476 # Handler for ActionView::MissingTemplate exception
501 477 def missing_template
502 478 logger.warn "Missing template, responding with 404"
503 479 @project = nil
504 480 render_404
505 481 end
506 482
507 483 # Filter for actions that provide an API response
508 484 # but have no HTML representation for non admin users
509 485 def require_admin_or_api_request
510 486 return true if api_request?
511 487 if User.current.admin?
512 488 true
513 489 elsif User.current.logged?
514 490 render_error(:status => 406)
515 491 else
516 492 deny_access
517 493 end
518 494 end
519 495
520 496 # Picks which layout to use based on the request
521 497 #
522 498 # @return [boolean, string] name of the layout to use or false for no layout
523 499 def use_layout
524 500 request.xhr? ? false : 'base'
525 501 end
526 502
527 503 def render_feed(items, options={})
528 504 @items = (items || []).to_a
529 505 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
530 506 @items = @items.slice(0, Setting.feeds_limit.to_i)
531 507 @title = options[:title] || Setting.app_title
532 508 render :template => "common/feed", :formats => [:atom], :layout => false,
533 509 :content_type => 'application/atom+xml'
534 510 end
535 511
536 512 def self.accept_rss_auth(*actions)
537 513 if actions.any?
538 514 self.accept_rss_auth_actions = actions
539 515 else
540 516 self.accept_rss_auth_actions || []
541 517 end
542 518 end
543 519
544 520 def accept_rss_auth?(action=action_name)
545 521 self.class.accept_rss_auth.include?(action.to_sym)
546 522 end
547 523
548 524 def self.accept_api_auth(*actions)
549 525 if actions.any?
550 526 self.accept_api_auth_actions = actions
551 527 else
552 528 self.accept_api_auth_actions || []
553 529 end
554 530 end
555 531
556 532 def accept_api_auth?(action=action_name)
557 533 self.class.accept_api_auth.include?(action.to_sym)
558 534 end
559 535
560 536 # Returns the number of objects that should be displayed
561 537 # on the paginated list
562 538 def per_page_option
563 539 per_page = nil
564 540 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
565 541 per_page = params[:per_page].to_s.to_i
566 542 session[:per_page] = per_page
567 543 elsif session[:per_page]
568 544 per_page = session[:per_page]
569 545 else
570 546 per_page = Setting.per_page_options_array.first || 25
571 547 end
572 548 per_page
573 549 end
574 550
575 551 # Returns offset and limit used to retrieve objects
576 552 # for an API response based on offset, limit and page parameters
577 553 def api_offset_and_limit(options=params)
578 554 if options[:offset].present?
579 555 offset = options[:offset].to_i
580 556 if offset < 0
581 557 offset = 0
582 558 end
583 559 end
584 560 limit = options[:limit].to_i
585 561 if limit < 1
586 562 limit = 25
587 563 elsif limit > 100
588 564 limit = 100
589 565 end
590 566 if offset.nil? && options[:page].present?
591 567 offset = (options[:page].to_i - 1) * limit
592 568 offset = 0 if offset < 0
593 569 end
594 570 offset ||= 0
595 571
596 572 [offset, limit]
597 573 end
598 574
599 575 # qvalues http header parser
600 576 # code taken from webrick
601 577 def parse_qvalues(value)
602 578 tmp = []
603 579 if value
604 580 parts = value.split(/,\s*/)
605 581 parts.each {|part|
606 582 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
607 583 val = m[1]
608 584 q = (m[2] or 1).to_f
609 585 tmp.push([val, q])
610 586 end
611 587 }
612 588 tmp = tmp.sort_by{|val, q| -q}
613 589 tmp.collect!{|val, q| val}
614 590 end
615 591 return tmp
616 592 rescue
617 593 nil
618 594 end
619 595
620 596 # Returns a string that can be used as filename value in Content-Disposition header
621 597 def filename_for_content_disposition(name)
622 598 request.env['HTTP_USER_AGENT'] =~ %r{(MSIE|Trident)} ? ERB::Util.url_encode(name) : name
623 599 end
624 600
625 601 def api_request?
626 602 %w(xml json).include? params[:format]
627 603 end
628 604
629 605 # Returns the API key present in the request
630 606 def api_key_from_request
631 607 if params[:key].present?
632 608 params[:key].to_s
633 609 elsif request.headers["X-Redmine-API-Key"].present?
634 610 request.headers["X-Redmine-API-Key"].to_s
635 611 end
636 612 end
637 613
638 614 # Returns the API 'switch user' value if present
639 615 def api_switch_user_from_request
640 616 request.headers["X-Redmine-Switch-User"].to_s.presence
641 617 end
642 618
643 619 # Renders a warning flash if obj has unsaved attachments
644 620 def render_attachment_warning_if_needed(obj)
645 621 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
646 622 end
647 623
648 624 # Rescues an invalid query statement. Just in case...
649 625 def query_statement_invalid(exception)
650 626 logger.error "Query::StatementInvalid: #{exception.message}" if logger
651 627 session.delete(:query)
652 628 sort_clear if respond_to?(:sort_clear)
653 629 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
654 630 end
655 631
656 632 # Renders a 200 response for successfull updates or deletions via the API
657 633 def render_api_ok
658 634 render_api_head :ok
659 635 end
660 636
661 637 # Renders a head API response
662 638 def render_api_head(status)
663 639 # #head would return a response body with one space
664 640 render :text => '', :status => status, :layout => nil
665 641 end
666 642
667 643 # Renders API response on validation failure
668 644 # for an object or an array of objects
669 645 def render_validation_errors(objects)
670 646 messages = Array.wrap(objects).map {|object| object.errors.full_messages}.flatten
671 647 render_api_errors(messages)
672 648 end
673 649
674 650 def render_api_errors(*messages)
675 651 @error_messages = messages.flatten
676 652 render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil
677 653 end
678 654
679 655 # Overrides #_include_layout? so that #render with no arguments
680 656 # doesn't use the layout for api requests
681 657 def _include_layout?(*args)
682 658 api_request? ? false : super
683 659 end
684 660 end
@@ -1,211 +1,210
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 class MyController < ApplicationController
19 19 before_filter :require_login
20 20 # let user change user's password when user has to
21 21 skip_before_filter :check_password_change, :only => :password
22 22
23 23 require_sudo_mode :account, only: :post
24 24 require_sudo_mode :reset_rss_key, :reset_api_key, :show_api_key, :destroy
25 25
26 26 helper :issues
27 27 helper :users
28 28 helper :custom_fields
29 29
30 30 BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues,
31 31 'issuesreportedbyme' => :label_reported_issues,
32 32 'issueswatched' => :label_watched_issues,
33 33 'news' => :label_news_latest,
34 34 'calendar' => :label_calendar,
35 35 'documents' => :label_document_plural,
36 36 'timelog' => :label_spent_time
37 37 }.merge(Redmine::Views::MyPage::Block.additional_blocks).freeze
38 38
39 39 DEFAULT_LAYOUT = { 'left' => ['issuesassignedtome'],
40 40 'right' => ['issuesreportedbyme']
41 41 }.freeze
42 42
43 43 def index
44 44 page
45 45 render :action => 'page'
46 46 end
47 47
48 48 # Show user's page
49 49 def page
50 50 @user = User.current
51 51 @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT
52 52 end
53 53
54 54 # Edit user's account
55 55 def account
56 56 @user = User.current
57 57 @pref = @user.pref
58 58 if request.post?
59 59 @user.safe_attributes = params[:user] if params[:user]
60 60 @user.pref.attributes = params[:pref] if params[:pref]
61 61 if @user.save
62 62 @user.pref.save
63 63 set_language_if_valid @user.language
64 64 flash[:notice] = l(:notice_account_updated)
65 65 redirect_to my_account_path
66 66 return
67 67 end
68 68 end
69 69 end
70 70
71 71 # Destroys user's account
72 72 def destroy
73 73 @user = User.current
74 74 unless @user.own_account_deletable?
75 75 redirect_to my_account_path
76 76 return
77 77 end
78 78
79 79 if request.post? && params[:confirm]
80 80 @user.destroy
81 81 if @user.destroyed?
82 82 logout_user
83 83 flash[:notice] = l(:notice_account_deleted)
84 84 end
85 85 redirect_to home_path
86 86 end
87 87 end
88 88
89 89 # Manage user's password
90 90 def password
91 91 @user = User.current
92 92 unless @user.change_password_allowed?
93 93 flash[:error] = l(:notice_can_t_change_password)
94 94 redirect_to my_account_path
95 95 return
96 96 end
97 97 if request.post?
98 98 if !@user.check_password?(params[:password])
99 99 flash.now[:error] = l(:notice_account_wrong_password)
100 100 elsif params[:password] == params[:new_password]
101 101 flash.now[:error] = l(:notice_new_password_must_be_different)
102 102 else
103 103 @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
104 104 @user.must_change_passwd = false
105 105 if @user.save
106 # Reset the session creation time to not log out this session on next
107 # request due to ApplicationController#force_logout_if_password_changed
108 session[:ctime] = User.current.passwd_changed_on.utc.to_i
106 # The session token was destroyed by the password change, generate a new one
107 session[:tk] = @user.generate_session_token
109 108 flash[:notice] = l(:notice_account_password_updated)
110 109 redirect_to my_account_path
111 110 end
112 111 end
113 112 end
114 113 end
115 114
116 115 # Create a new feeds key
117 116 def reset_rss_key
118 117 if request.post?
119 118 if User.current.rss_token
120 119 User.current.rss_token.destroy
121 120 User.current.reload
122 121 end
123 122 User.current.rss_key
124 123 flash[:notice] = l(:notice_feeds_access_key_reseted)
125 124 end
126 125 redirect_to my_account_path
127 126 end
128 127
129 128 def show_api_key
130 129 @user = User.current
131 130 end
132 131
133 132 # Create a new API key
134 133 def reset_api_key
135 134 if request.post?
136 135 if User.current.api_token
137 136 User.current.api_token.destroy
138 137 User.current.reload
139 138 end
140 139 User.current.api_key
141 140 flash[:notice] = l(:notice_api_access_key_reseted)
142 141 end
143 142 redirect_to my_account_path
144 143 end
145 144
146 145 # User's page layout configuration
147 146 def page_layout
148 147 @user = User.current
149 148 @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup
150 149 @block_options = []
151 150 BLOCKS.each do |k, v|
152 151 unless @blocks.values.flatten.include?(k)
153 152 @block_options << [l("my.blocks.#{v}", :default => [v, v.to_s.humanize]), k.dasherize]
154 153 end
155 154 end
156 155 end
157 156
158 157 # Add a block to user's page
159 158 # The block is added on top of the page
160 159 # params[:block] : id of the block to add
161 160 def add_block
162 161 block = params[:block].to_s.underscore
163 162 if block.present? && BLOCKS.key?(block)
164 163 @user = User.current
165 164 layout = @user.pref[:my_page_layout] || {}
166 165 # remove if already present in a group
167 166 %w(top left right).each {|f| (layout[f] ||= []).delete block }
168 167 # add it on top
169 168 layout['top'].unshift block
170 169 @user.pref[:my_page_layout] = layout
171 170 @user.pref.save
172 171 end
173 172 redirect_to my_page_layout_path
174 173 end
175 174
176 175 # Remove a block to user's page
177 176 # params[:block] : id of the block to remove
178 177 def remove_block
179 178 block = params[:block].to_s.underscore
180 179 @user = User.current
181 180 # remove block in all groups
182 181 layout = @user.pref[:my_page_layout] || {}
183 182 %w(top left right).each {|f| (layout[f] ||= []).delete block }
184 183 @user.pref[:my_page_layout] = layout
185 184 @user.pref.save
186 185 redirect_to my_page_layout_path
187 186 end
188 187
189 188 # Change blocks order on user's page
190 189 # params[:group] : group to order (top, left or right)
191 190 # params[:list-(top|left|right)] : array of block ids of the group
192 191 def order_blocks
193 192 group = params[:group]
194 193 @user = User.current
195 194 if group.is_a?(String)
196 195 group_items = (params["blocks"] || []).collect(&:underscore)
197 196 group_items.each {|s| s.sub!(/^block_/, '')}
198 197 if group_items and group_items.is_a? Array
199 198 layout = @user.pref[:my_page_layout] || {}
200 199 # remove group blocks if they are presents in other groups
201 200 %w(top left right).each {|f|
202 201 layout[f] = (layout[f] || []) - group_items
203 202 }
204 203 layout[group] = group_items
205 204 @user.pref[:my_page_layout] = layout
206 205 @user.pref.save
207 206 end
208 207 end
209 208 render :nothing => true
210 209 end
211 210 end
@@ -1,85 +1,93
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 class Token < ActiveRecord::Base
19 19 belongs_to :user
20 20 validates_uniqueness_of :value
21 21 attr_protected :id
22 22
23 23 before_create :delete_previous_tokens, :generate_new_token
24 24
25 25 cattr_accessor :validity_time
26 26 self.validity_time = 1.day
27 27
28 28 def generate_new_token
29 29 self.value = Token.generate_token_value
30 30 end
31 31
32 32 # Return true if token has expired
33 33 def expired?
34 34 return Time.now > self.created_on + self.class.validity_time
35 35 end
36 36
37 37 # Delete all expired tokens
38 38 def self.destroy_expired
39 Token.where("action NOT IN (?) AND created_on < ?", ['feeds', 'api'], Time.now - validity_time).delete_all
39 Token.where("action NOT IN (?) AND created_on < ?", ['feeds', 'api', 'session'], Time.now - validity_time).delete_all
40 40 end
41 41
42 42 # Returns the active user who owns the key for the given action
43 43 def self.find_active_user(action, key, validity_days=nil)
44 44 user = find_user(action, key, validity_days)
45 45 if user && user.active?
46 46 user
47 47 end
48 48 end
49 49
50 50 # Returns the user who owns the key for the given action
51 51 def self.find_user(action, key, validity_days=nil)
52 52 token = find_token(action, key, validity_days)
53 53 if token
54 54 token.user
55 55 end
56 56 end
57 57
58 58 # Returns the token for action and key with an optional
59 59 # validity duration (in number of days)
60 60 def self.find_token(action, key, validity_days=nil)
61 61 action = action.to_s
62 62 key = key.to_s
63 63 return nil unless action.present? && key =~ /\A[a-z0-9]+\z/i
64 64
65 65 token = Token.where(:action => action, :value => key).first
66 66 if token && (token.action == action) && (token.value == key) && token.user
67 67 if validity_days.nil? || (token.created_on > validity_days.days.ago)
68 68 token
69 69 end
70 70 end
71 71 end
72 72
73 73 def self.generate_token_value
74 74 Redmine::Utils.random_hex(20)
75 75 end
76 76
77 77 private
78 78
79 79 # Removes obsolete tokens (same user and action)
80 80 def delete_previous_tokens
81 81 if user
82 Token.where(:user_id => user.id, :action => action).delete_all
82 scope = Token.where(:user_id => user.id, :action => action)
83 if action == 'session'
84 ids = scope.order(:updated_on => :desc).offset(9).ids
85 if ids.any?
86 Token.delete(ids)
87 end
88 else
89 scope.delete_all
90 end
83 91 end
84 92 end
85 93 end
@@ -1,861 +1,881
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 # Different ways of displaying/sorting users
24 24 USER_FORMATS = {
25 25 :firstname_lastname => {
26 26 :string => '#{firstname} #{lastname}',
27 27 :order => %w(firstname lastname id),
28 28 :setting_order => 1
29 29 },
30 30 :firstname_lastinitial => {
31 31 :string => '#{firstname} #{lastname.to_s.chars.first}.',
32 32 :order => %w(firstname lastname id),
33 33 :setting_order => 2
34 34 },
35 35 :firstinitial_lastname => {
36 36 :string => '#{firstname.to_s.gsub(/(([[:alpha:]])[[:alpha:]]*\.?)/, \'\2.\')} #{lastname}',
37 37 :order => %w(firstname lastname id),
38 38 :setting_order => 2
39 39 },
40 40 :firstname => {
41 41 :string => '#{firstname}',
42 42 :order => %w(firstname id),
43 43 :setting_order => 3
44 44 },
45 45 :lastname_firstname => {
46 46 :string => '#{lastname} #{firstname}',
47 47 :order => %w(lastname firstname id),
48 48 :setting_order => 4
49 49 },
50 50 :lastname_comma_firstname => {
51 51 :string => '#{lastname}, #{firstname}',
52 52 :order => %w(lastname firstname id),
53 53 :setting_order => 5
54 54 },
55 55 :lastname => {
56 56 :string => '#{lastname}',
57 57 :order => %w(lastname id),
58 58 :setting_order => 6
59 59 },
60 60 :username => {
61 61 :string => '#{login}',
62 62 :order => %w(login id),
63 63 :setting_order => 7
64 64 },
65 65 }
66 66
67 67 MAIL_NOTIFICATION_OPTIONS = [
68 68 ['all', :label_user_mail_option_all],
69 69 ['selected', :label_user_mail_option_selected],
70 70 ['only_my_events', :label_user_mail_option_only_my_events],
71 71 ['only_assigned', :label_user_mail_option_only_assigned],
72 72 ['only_owner', :label_user_mail_option_only_owner],
73 73 ['none', :label_user_mail_option_none]
74 74 ]
75 75
76 76 has_and_belongs_to_many :groups,
77 77 :join_table => "#{table_name_prefix}groups_users#{table_name_suffix}",
78 78 :after_add => Proc.new {|user, group| group.user_added(user)},
79 79 :after_remove => Proc.new {|user, group| group.user_removed(user)}
80 80 has_many :changesets, :dependent => :nullify
81 81 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
82 82 has_one :rss_token, lambda {where "action='feeds'"}, :class_name => 'Token'
83 83 has_one :api_token, lambda {where "action='api'"}, :class_name => 'Token'
84 84 has_one :email_address, lambda {where :is_default => true}, :autosave => true
85 85 has_many :email_addresses, :dependent => :delete_all
86 86 belongs_to :auth_source
87 87
88 88 scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
89 89 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
90 90
91 91 acts_as_customizable
92 92
93 93 attr_accessor :password, :password_confirmation, :generate_password
94 94 attr_accessor :last_before_login_on
95 95 # Prevents unauthorized assignments
96 96 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
97 97
98 98 LOGIN_LENGTH_LIMIT = 60
99 99 MAIL_LENGTH_LIMIT = 60
100 100
101 101 validates_presence_of :login, :firstname, :lastname, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
102 102 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
103 103 # Login must contain letters, numbers, underscores only
104 104 validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i
105 105 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
106 106 validates_length_of :firstname, :lastname, :maximum => 30
107 107 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
108 108 validate :validate_password_length
109 109 validate do
110 110 if password_confirmation && password != password_confirmation
111 111 errors.add(:password, :confirmation)
112 112 end
113 113 end
114 114
115 115 before_validation :instantiate_email_address
116 116 before_create :set_mail_notification
117 117 before_save :generate_password_if_needed, :update_hashed_password
118 118 before_destroy :remove_references_before_destroy
119 119 after_save :update_notified_project_ids, :destroy_tokens
120 120
121 121 scope :in_group, lambda {|group|
122 122 group_id = group.is_a?(Group) ? group.id : group.to_i
123 123 where("#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
124 124 }
125 125 scope :not_in_group, lambda {|group|
126 126 group_id = group.is_a?(Group) ? group.id : group.to_i
127 127 where("#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
128 128 }
129 129 scope :sorted, lambda { order(*User.fields_for_order_statement)}
130 130 scope :having_mail, lambda {|arg|
131 131 addresses = Array.wrap(arg).map {|a| a.to_s.downcase}
132 132 if addresses.any?
133 133 joins(:email_addresses).where("LOWER(#{EmailAddress.table_name}.address) IN (?)", addresses).uniq
134 134 else
135 135 none
136 136 end
137 137 }
138 138
139 139 def set_mail_notification
140 140 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
141 141 true
142 142 end
143 143
144 144 def update_hashed_password
145 145 # update hashed_password if password was set
146 146 if self.password && self.auth_source_id.blank?
147 147 salt_password(password)
148 148 end
149 149 end
150 150
151 151 alias :base_reload :reload
152 152 def reload(*args)
153 153 @name = nil
154 154 @projects_by_role = nil
155 155 @membership_by_project_id = nil
156 156 @notified_projects_ids = nil
157 157 @notified_projects_ids_changed = false
158 158 @builtin_role = nil
159 159 @visible_project_ids = nil
160 160 @managed_roles = nil
161 161 base_reload(*args)
162 162 end
163 163
164 164 def mail
165 165 email_address.try(:address)
166 166 end
167 167
168 168 def mail=(arg)
169 169 email = email_address || build_email_address
170 170 email.address = arg
171 171 end
172 172
173 173 def mail_changed?
174 174 email_address.try(:address_changed?)
175 175 end
176 176
177 177 def mails
178 178 email_addresses.pluck(:address)
179 179 end
180 180
181 181 def self.find_or_initialize_by_identity_url(url)
182 182 user = where(:identity_url => url).first
183 183 unless user
184 184 user = User.new
185 185 user.identity_url = url
186 186 end
187 187 user
188 188 end
189 189
190 190 def identity_url=(url)
191 191 if url.blank?
192 192 write_attribute(:identity_url, '')
193 193 else
194 194 begin
195 195 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
196 196 rescue OpenIdAuthentication::InvalidOpenId
197 197 # Invalid url, don't save
198 198 end
199 199 end
200 200 self.read_attribute(:identity_url)
201 201 end
202 202
203 203 # Returns the user that matches provided login and password, or nil
204 204 def self.try_to_login(login, password, active_only=true)
205 205 login = login.to_s
206 206 password = password.to_s
207 207
208 208 # Make sure no one can sign in with an empty login or password
209 209 return nil if login.empty? || password.empty?
210 210 user = find_by_login(login)
211 211 if user
212 212 # user is already in local database
213 213 return nil unless user.check_password?(password)
214 214 return nil if !user.active? && active_only
215 215 else
216 216 # user is not yet registered, try to authenticate with available sources
217 217 attrs = AuthSource.authenticate(login, password)
218 218 if attrs
219 219 user = new(attrs)
220 220 user.login = login
221 221 user.language = Setting.default_language
222 222 if user.save
223 223 user.reload
224 224 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
225 225 end
226 226 end
227 227 end
228 228 user.update_column(:last_login_on, Time.now) if user && !user.new_record? && user.active?
229 229 user
230 230 rescue => text
231 231 raise text
232 232 end
233 233
234 234 # Returns the user who matches the given autologin +key+ or nil
235 235 def self.try_to_autologin(key)
236 236 user = Token.find_active_user('autologin', key, Setting.autologin.to_i)
237 237 if user
238 238 user.update_column(:last_login_on, Time.now)
239 239 user
240 240 end
241 241 end
242 242
243 243 def self.name_formatter(formatter = nil)
244 244 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
245 245 end
246 246
247 247 # Returns an array of fields names than can be used to make an order statement for users
248 248 # according to how user names are displayed
249 249 # Examples:
250 250 #
251 251 # User.fields_for_order_statement => ['users.login', 'users.id']
252 252 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
253 253 def self.fields_for_order_statement(table=nil)
254 254 table ||= table_name
255 255 name_formatter[:order].map {|field| "#{table}.#{field}"}
256 256 end
257 257
258 258 # Return user's full name for display
259 259 def name(formatter = nil)
260 260 f = self.class.name_formatter(formatter)
261 261 if formatter
262 262 eval('"' + f[:string] + '"')
263 263 else
264 264 @name ||= eval('"' + f[:string] + '"')
265 265 end
266 266 end
267 267
268 268 def active?
269 269 self.status == STATUS_ACTIVE
270 270 end
271 271
272 272 def registered?
273 273 self.status == STATUS_REGISTERED
274 274 end
275 275
276 276 def locked?
277 277 self.status == STATUS_LOCKED
278 278 end
279 279
280 280 def activate
281 281 self.status = STATUS_ACTIVE
282 282 end
283 283
284 284 def register
285 285 self.status = STATUS_REGISTERED
286 286 end
287 287
288 288 def lock
289 289 self.status = STATUS_LOCKED
290 290 end
291 291
292 292 def activate!
293 293 update_attribute(:status, STATUS_ACTIVE)
294 294 end
295 295
296 296 def register!
297 297 update_attribute(:status, STATUS_REGISTERED)
298 298 end
299 299
300 300 def lock!
301 301 update_attribute(:status, STATUS_LOCKED)
302 302 end
303 303
304 304 # Returns true if +clear_password+ is the correct user's password, otherwise false
305 305 def check_password?(clear_password)
306 306 if auth_source_id.present?
307 307 auth_source.authenticate(self.login, clear_password)
308 308 else
309 309 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
310 310 end
311 311 end
312 312
313 313 # Generates a random salt and computes hashed_password for +clear_password+
314 314 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
315 315 def salt_password(clear_password)
316 316 self.salt = User.generate_salt
317 317 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
318 318 self.passwd_changed_on = Time.now.change(:usec => 0)
319 319 end
320 320
321 321 # Does the backend storage allow this user to change their password?
322 322 def change_password_allowed?
323 323 return true if auth_source.nil?
324 324 return auth_source.allow_password_changes?
325 325 end
326 326
327 327 # Returns true if the user password has expired
328 328 def password_expired?
329 329 period = Setting.password_max_age.to_i
330 330 if period.zero?
331 331 false
332 332 else
333 333 changed_on = self.passwd_changed_on || Time.at(0)
334 334 changed_on < period.days.ago
335 335 end
336 336 end
337 337
338 338 def must_change_password?
339 339 (must_change_passwd? || password_expired?) && change_password_allowed?
340 340 end
341 341
342 342 def generate_password?
343 343 generate_password == '1' || generate_password == true
344 344 end
345 345
346 346 # Generate and set a random password on given length
347 347 def random_password(length=40)
348 348 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
349 349 chars -= %w(0 O 1 l)
350 350 password = ''
351 351 length.times {|i| password << chars[SecureRandom.random_number(chars.size)] }
352 352 self.password = password
353 353 self.password_confirmation = password
354 354 self
355 355 end
356 356
357 357 def pref
358 358 self.preference ||= UserPreference.new(:user => self)
359 359 end
360 360
361 361 def time_zone
362 362 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
363 363 end
364 364
365 365 def force_default_language?
366 366 Setting.force_default_language_for_loggedin?
367 367 end
368 368
369 369 def language
370 370 if force_default_language?
371 371 Setting.default_language
372 372 else
373 373 super
374 374 end
375 375 end
376 376
377 377 def wants_comments_in_reverse_order?
378 378 self.pref[:comments_sorting] == 'desc'
379 379 end
380 380
381 381 # Return user's RSS key (a 40 chars long string), used to access feeds
382 382 def rss_key
383 383 if rss_token.nil?
384 384 create_rss_token(:action => 'feeds')
385 385 end
386 386 rss_token.value
387 387 end
388 388
389 389 # Return user's API key (a 40 chars long string), used to access the API
390 390 def api_key
391 391 if api_token.nil?
392 392 create_api_token(:action => 'api')
393 393 end
394 394 api_token.value
395 395 end
396 396
397 # Generates a new session token and returns its value
398 def generate_session_token
399 token = Token.create!(:user_id => id, :action => 'session')
400 token.value
401 end
402
403 # Returns true if token is a valid session token for the user whose id is user_id
404 def self.verify_session_token(user_id, token)
405 return false if user_id.blank? || token.blank?
406
407 scope = Token.where(:user_id => user_id, :value => token.to_s, :action => 'session')
408 if Setting.session_lifetime?
409 scope = scope.where("created_on > ?", Setting.session_lifetime.to_i.minutes.ago)
410 end
411 if Setting.session_timeout?
412 scope = scope.where("updated_on > ?", Setting.session_timeout.to_i.minutes.ago)
413 end
414 scope.update_all(:updated_on => Time.now) == 1
415 end
416
397 417 # Return an array of project ids for which the user has explicitly turned mail notifications on
398 418 def notified_projects_ids
399 419 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
400 420 end
401 421
402 422 def notified_project_ids=(ids)
403 423 @notified_projects_ids_changed = true
404 424 @notified_projects_ids = ids.map(&:to_i).uniq.select {|n| n > 0}
405 425 end
406 426
407 427 # Updates per project notifications (after_save callback)
408 428 def update_notified_project_ids
409 429 if @notified_projects_ids_changed
410 430 ids = (mail_notification == 'selected' ? Array.wrap(notified_projects_ids).reject(&:blank?) : [])
411 431 members.update_all(:mail_notification => false)
412 432 members.where(:project_id => ids).update_all(:mail_notification => true) if ids.any?
413 433 end
414 434 end
415 435 private :update_notified_project_ids
416 436
417 437 def valid_notification_options
418 438 self.class.valid_notification_options(self)
419 439 end
420 440
421 441 # Only users that belong to more than 1 project can select projects for which they are notified
422 442 def self.valid_notification_options(user=nil)
423 443 # Note that @user.membership.size would fail since AR ignores
424 444 # :include association option when doing a count
425 445 if user.nil? || user.memberships.length < 1
426 446 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
427 447 else
428 448 MAIL_NOTIFICATION_OPTIONS
429 449 end
430 450 end
431 451
432 452 # Find a user account by matching the exact login and then a case-insensitive
433 453 # version. Exact matches will be given priority.
434 454 def self.find_by_login(login)
435 455 login = Redmine::CodesetUtil.replace_invalid_utf8(login.to_s)
436 456 if login.present?
437 457 # First look for an exact match
438 458 user = where(:login => login).detect {|u| u.login == login}
439 459 unless user
440 460 # Fail over to case-insensitive if none was found
441 461 user = where("LOWER(login) = ?", login.downcase).first
442 462 end
443 463 user
444 464 end
445 465 end
446 466
447 467 def self.find_by_rss_key(key)
448 468 Token.find_active_user('feeds', key)
449 469 end
450 470
451 471 def self.find_by_api_key(key)
452 472 Token.find_active_user('api', key)
453 473 end
454 474
455 475 # Makes find_by_mail case-insensitive
456 476 def self.find_by_mail(mail)
457 477 having_mail(mail).first
458 478 end
459 479
460 480 # Returns true if the default admin account can no longer be used
461 481 def self.default_admin_account_changed?
462 482 !User.active.find_by_login("admin").try(:check_password?, "admin")
463 483 end
464 484
465 485 def to_s
466 486 name
467 487 end
468 488
469 489 CSS_CLASS_BY_STATUS = {
470 490 STATUS_ANONYMOUS => 'anon',
471 491 STATUS_ACTIVE => 'active',
472 492 STATUS_REGISTERED => 'registered',
473 493 STATUS_LOCKED => 'locked'
474 494 }
475 495
476 496 def css_classes
477 497 "user #{CSS_CLASS_BY_STATUS[status]}"
478 498 end
479 499
480 500 # Returns the current day according to user's time zone
481 501 def today
482 502 if time_zone.nil?
483 503 Date.today
484 504 else
485 505 Time.now.in_time_zone(time_zone).to_date
486 506 end
487 507 end
488 508
489 509 # Returns the day of +time+ according to user's time zone
490 510 def time_to_date(time)
491 511 if time_zone.nil?
492 512 time.to_date
493 513 else
494 514 time.in_time_zone(time_zone).to_date
495 515 end
496 516 end
497 517
498 518 def logged?
499 519 true
500 520 end
501 521
502 522 def anonymous?
503 523 !logged?
504 524 end
505 525
506 526 # Returns user's membership for the given project
507 527 # or nil if the user is not a member of project
508 528 def membership(project)
509 529 project_id = project.is_a?(Project) ? project.id : project
510 530
511 531 @membership_by_project_id ||= Hash.new {|h, project_id|
512 532 h[project_id] = memberships.where(:project_id => project_id).first
513 533 }
514 534 @membership_by_project_id[project_id]
515 535 end
516 536
517 537 # Returns the user's bult-in role
518 538 def builtin_role
519 539 @builtin_role ||= Role.non_member
520 540 end
521 541
522 542 # Return user's roles for project
523 543 def roles_for_project(project)
524 544 # No role on archived projects
525 545 return [] if project.nil? || project.archived?
526 546 if membership = membership(project)
527 547 membership.roles.dup
528 548 elsif project.is_public?
529 549 project.override_roles(builtin_role)
530 550 else
531 551 []
532 552 end
533 553 end
534 554
535 555 # Returns a hash of user's projects grouped by roles
536 556 def projects_by_role
537 557 return @projects_by_role if @projects_by_role
538 558
539 559 hash = Hash.new([])
540 560
541 561 group_class = anonymous? ? GroupAnonymous : GroupNonMember
542 562 members = Member.joins(:project, :principal).
543 563 where("#{Project.table_name}.status <> 9").
544 564 where("#{Member.table_name}.user_id = ? OR (#{Project.table_name}.is_public = ? AND #{Principal.table_name}.type = ?)", self.id, true, group_class.name).
545 565 preload(:project, :roles).
546 566 to_a
547 567
548 568 members.reject! {|member| member.user_id != id && project_ids.include?(member.project_id)}
549 569 members.each do |member|
550 570 if member.project
551 571 member.roles.each do |role|
552 572 hash[role] = [] unless hash.key?(role)
553 573 hash[role] << member.project
554 574 end
555 575 end
556 576 end
557 577
558 578 hash.each do |role, projects|
559 579 projects.uniq!
560 580 end
561 581
562 582 @projects_by_role = hash
563 583 end
564 584
565 585 # Returns the ids of visible projects
566 586 def visible_project_ids
567 587 @visible_project_ids ||= Project.visible(self).pluck(:id)
568 588 end
569 589
570 590 # Returns the roles that the user is allowed to manage for the given project
571 591 def managed_roles(project)
572 592 if admin?
573 593 @managed_roles ||= Role.givable.to_a
574 594 else
575 595 membership(project).try(:managed_roles) || []
576 596 end
577 597 end
578 598
579 599 # Returns true if user is arg or belongs to arg
580 600 def is_or_belongs_to?(arg)
581 601 if arg.is_a?(User)
582 602 self == arg
583 603 elsif arg.is_a?(Group)
584 604 arg.users.include?(self)
585 605 else
586 606 false
587 607 end
588 608 end
589 609
590 610 # Return true if the user is allowed to do the specified action on a specific context
591 611 # Action can be:
592 612 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
593 613 # * a permission Symbol (eg. :edit_project)
594 614 # Context can be:
595 615 # * a project : returns true if user is allowed to do the specified action on this project
596 616 # * an array of projects : returns true if user is allowed on every project
597 617 # * nil with options[:global] set : check if user has at least one role allowed for this action,
598 618 # or falls back to Non Member / Anonymous permissions depending if the user is logged
599 619 def allowed_to?(action, context, options={}, &block)
600 620 if context && context.is_a?(Project)
601 621 return false unless context.allows_to?(action)
602 622 # Admin users are authorized for anything else
603 623 return true if admin?
604 624
605 625 roles = roles_for_project(context)
606 626 return false unless roles
607 627 roles.any? {|role|
608 628 (context.is_public? || role.member?) &&
609 629 role.allowed_to?(action) &&
610 630 (block_given? ? yield(role, self) : true)
611 631 }
612 632 elsif context && context.is_a?(Array)
613 633 if context.empty?
614 634 false
615 635 else
616 636 # Authorize if user is authorized on every element of the array
617 637 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
618 638 end
619 639 elsif context
620 640 raise ArgumentError.new("#allowed_to? context argument must be a Project, an Array of projects or nil")
621 641 elsif options[:global]
622 642 # Admin users are always authorized
623 643 return true if admin?
624 644
625 645 # authorize if user has at least one role that has this permission
626 646 roles = memberships.collect {|m| m.roles}.flatten.uniq
627 647 roles << (self.logged? ? Role.non_member : Role.anonymous)
628 648 roles.any? {|role|
629 649 role.allowed_to?(action) &&
630 650 (block_given? ? yield(role, self) : true)
631 651 }
632 652 else
633 653 false
634 654 end
635 655 end
636 656
637 657 # Is the user allowed to do the specified action on any project?
638 658 # See allowed_to? for the actions and valid options.
639 659 #
640 660 # NB: this method is not used anywhere in the core codebase as of
641 661 # 2.5.2, but it's used by many plugins so if we ever want to remove
642 662 # it it has to be carefully deprecated for a version or two.
643 663 def allowed_to_globally?(action, options={}, &block)
644 664 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
645 665 end
646 666
647 667 def allowed_to_view_all_time_entries?(context)
648 668 allowed_to?(:view_time_entries, context) do |role, user|
649 669 role.time_entries_visibility == 'all'
650 670 end
651 671 end
652 672
653 673 # Returns true if the user is allowed to delete the user's own account
654 674 def own_account_deletable?
655 675 Setting.unsubscribe? &&
656 676 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
657 677 end
658 678
659 679 safe_attributes 'firstname',
660 680 'lastname',
661 681 'mail',
662 682 'mail_notification',
663 683 'notified_project_ids',
664 684 'language',
665 685 'custom_field_values',
666 686 'custom_fields',
667 687 'identity_url'
668 688
669 689 safe_attributes 'status',
670 690 'auth_source_id',
671 691 'generate_password',
672 692 'must_change_passwd',
673 693 :if => lambda {|user, current_user| current_user.admin?}
674 694
675 695 safe_attributes 'group_ids',
676 696 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
677 697
678 698 # Utility method to help check if a user should be notified about an
679 699 # event.
680 700 #
681 701 # TODO: only supports Issue events currently
682 702 def notify_about?(object)
683 703 if mail_notification == 'all'
684 704 true
685 705 elsif mail_notification.blank? || mail_notification == 'none'
686 706 false
687 707 else
688 708 case object
689 709 when Issue
690 710 case mail_notification
691 711 when 'selected', 'only_my_events'
692 712 # user receives notifications for created/assigned issues on unselected projects
693 713 object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
694 714 when 'only_assigned'
695 715 is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
696 716 when 'only_owner'
697 717 object.author == self
698 718 end
699 719 when News
700 720 # always send to project members except when mail_notification is set to 'none'
701 721 true
702 722 end
703 723 end
704 724 end
705 725
706 726 def self.current=(user)
707 727 RequestStore.store[:current_user] = user
708 728 end
709 729
710 730 def self.current
711 731 RequestStore.store[:current_user] ||= User.anonymous
712 732 end
713 733
714 734 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
715 735 # one anonymous user per database.
716 736 def self.anonymous
717 737 anonymous_user = AnonymousUser.first
718 738 if anonymous_user.nil?
719 739 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :login => '', :status => 0)
720 740 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
721 741 end
722 742 anonymous_user
723 743 end
724 744
725 745 # Salts all existing unsalted passwords
726 746 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
727 747 # This method is used in the SaltPasswords migration and is to be kept as is
728 748 def self.salt_unsalted_passwords!
729 749 transaction do
730 750 User.where("salt IS NULL OR salt = ''").find_each do |user|
731 751 next if user.hashed_password.blank?
732 752 salt = User.generate_salt
733 753 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
734 754 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
735 755 end
736 756 end
737 757 end
738 758
739 759 protected
740 760
741 761 def validate_password_length
742 762 return if password.blank? && generate_password?
743 763 # Password length validation based on setting
744 764 if !password.nil? && password.size < Setting.password_min_length.to_i
745 765 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
746 766 end
747 767 end
748 768
749 769 def instantiate_email_address
750 770 email_address || build_email_address
751 771 end
752 772
753 773 private
754 774
755 775 def generate_password_if_needed
756 776 if generate_password? && auth_source.nil?
757 777 length = [Setting.password_min_length.to_i + 2, 10].max
758 778 random_password(length)
759 779 end
760 780 end
761 781
762 782 # Delete all outstanding password reset tokens on password change.
763 783 # Delete the autologin tokens on password change to prohibit session leakage.
764 784 # This helps to keep the account secure in case the associated email account
765 785 # was compromised.
766 786 def destroy_tokens
767 if hashed_password_changed?
768 tokens = ['recovery', 'autologin']
787 if hashed_password_changed? || (status_changed? && !active?)
788 tokens = ['recovery', 'autologin', 'session']
769 789 Token.where(:user_id => id, :action => tokens).delete_all
770 790 end
771 791 end
772 792
773 793 # Removes references that are not handled by associations
774 794 # Things that are not deleted are reassociated with the anonymous user
775 795 def remove_references_before_destroy
776 796 return if self.id.nil?
777 797
778 798 substitute = User.anonymous
779 799 Attachment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
780 800 Comment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
781 801 Issue.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
782 802 Issue.where(['assigned_to_id = ?', id]).update_all('assigned_to_id = NULL')
783 803 Journal.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
784 804 JournalDetail.
785 805 where(["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]).
786 806 update_all(['old_value = ?', substitute.id.to_s])
787 807 JournalDetail.
788 808 where(["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]).
789 809 update_all(['value = ?', substitute.id.to_s])
790 810 Message.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
791 811 News.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
792 812 # Remove private queries and keep public ones
793 813 ::Query.delete_all ['user_id = ? AND visibility = ?', id, ::Query::VISIBILITY_PRIVATE]
794 814 ::Query.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
795 815 TimeEntry.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
796 816 Token.delete_all ['user_id = ?', id]
797 817 Watcher.delete_all ['user_id = ?', id]
798 818 WikiContent.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
799 819 WikiContent::Version.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
800 820 end
801 821
802 822 # Return password digest
803 823 def self.hash_password(clear_password)
804 824 Digest::SHA1.hexdigest(clear_password || "")
805 825 end
806 826
807 827 # Returns a 128bits random salt as a hex string (32 chars long)
808 828 def self.generate_salt
809 829 Redmine::Utils.random_hex(16)
810 830 end
811 831
812 832 end
813 833
814 834 class AnonymousUser < User
815 835 validate :validate_anonymous_uniqueness, :on => :create
816 836
817 837 def validate_anonymous_uniqueness
818 838 # There should be only one AnonymousUser in the database
819 839 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
820 840 end
821 841
822 842 def available_custom_fields
823 843 []
824 844 end
825 845
826 846 # Overrides a few properties
827 847 def logged?; false end
828 848 def admin; false end
829 849 def name(*args); I18n.t(:label_user_anonymous) end
830 850 def mail=(*args); nil end
831 851 def mail; nil end
832 852 def time_zone; nil end
833 853 def rss_key; nil end
834 854
835 855 def pref
836 856 UserPreference.new(:user => self)
837 857 end
838 858
839 859 # Returns the user's bult-in role
840 860 def builtin_role
841 861 @builtin_role ||= Role.anonymous
842 862 end
843 863
844 864 def membership(*args)
845 865 nil
846 866 end
847 867
848 868 def member_of?(*args)
849 869 false
850 870 end
851 871
852 872 # Anonymous user can not be destroyed
853 873 def destroy
854 874 false
855 875 end
856 876
857 877 protected
858 878
859 879 def instantiate_email_address
860 880 end
861 881 end
@@ -1,35 +1,38
1 1 Rails.application.configure do
2 2 # Settings specified here will take precedence over those in config/application.rb
3 3
4 4 # The test environment is used exclusively to run your application's
5 5 # test suite. You never need to work with it otherwise. Remember that
6 6 # your test database is "scratch space" for the test suite and is wiped
7 7 # and recreated between test runs. Don't rely on the data there!
8 8 config.cache_classes = true
9 9
10 10 # Do not eager load code on boot. This avoids loading your whole application
11 11 # just for the purpose of running a single test. If you are using a tool that
12 12 # preloads Rails for running tests, you may have to set it to true.
13 13 config.eager_load = false
14 14
15 15 # Show full error reports and disable caching
16 16 config.consider_all_requests_local = true
17 17 config.action_controller.perform_caching = false
18 18
19 19 config.action_mailer.perform_deliveries = true
20 20
21 21 # Tell Action Mailer not to deliver emails to the real world.
22 22 # The :test delivery method accumulates sent emails in the
23 23 # ActionMailer::Base.deliveries array.
24 24 config.action_mailer.delivery_method = :test
25 25
26 26 # Disable request forgery protection in test environment.
27 27 config.action_controller.allow_forgery_protection = false
28 28
29 # Disable sessions verifications in test environment.
30 config.redmine_verify_sessions = false
31
29 32 # Print deprecation notices to stderr and the Rails logger.
30 33 config.active_support.deprecation = [:stderr, :log]
31 34
32 35 config.secret_key_base = 'a secret token for running the tests'
33 36
34 37 config.active_support.test_order = :random
35 38 end
@@ -1,280 +1,268
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 MyControllerTest < ActionController::TestCase
21 21 fixtures :users, :email_addresses, :user_preferences, :roles, :projects, :members, :member_roles,
22 22 :issues, :issue_statuses, :trackers, :enumerations, :custom_fields, :auth_sources
23 23
24 24 def setup
25 25 @request.session[:user_id] = 2
26 26 end
27 27
28 28 def test_index
29 29 get :index
30 30 assert_response :success
31 31 assert_template 'page'
32 32 end
33 33
34 34 def test_page
35 35 get :page
36 36 assert_response :success
37 37 assert_template 'page'
38 38 end
39 39
40 40 def test_page_with_timelog_block
41 41 preferences = User.find(2).pref
42 42 preferences[:my_page_layout] = {'top' => ['timelog']}
43 43 preferences.save!
44 44 TimeEntry.create!(:user => User.find(2), :spent_on => Date.yesterday, :issue_id => 1, :hours => 2.5, :activity_id => 10)
45 45
46 46 get :page
47 47 assert_response :success
48 48 assert_select 'tr.time-entry' do
49 49 assert_select 'td.subject a[href="/issues/1"]'
50 50 assert_select 'td.hours', :text => '2.50'
51 51 end
52 52 end
53 53
54 54 def test_page_with_all_blocks
55 55 blocks = MyController::BLOCKS.keys
56 56 preferences = User.find(2).pref
57 57 preferences[:my_page_layout] = {'top' => blocks}
58 58 preferences.save!
59 59
60 60 get :page
61 61 assert_response :success
62 62 assert_select 'div.mypage-box', blocks.size
63 63 end
64 64
65 65 def test_my_account_should_show_editable_custom_fields
66 66 get :account
67 67 assert_response :success
68 68 assert_template 'account'
69 69 assert_equal User.find(2), assigns(:user)
70 70
71 71 assert_select 'input[name=?]', 'user[custom_field_values][4]'
72 72 end
73 73
74 74 def test_my_account_should_not_show_non_editable_custom_fields
75 75 UserCustomField.find(4).update_attribute :editable, false
76 76
77 77 get :account
78 78 assert_response :success
79 79 assert_template 'account'
80 80 assert_equal User.find(2), assigns(:user)
81 81
82 82 assert_select 'input[name=?]', 'user[custom_field_values][4]', 0
83 83 end
84 84
85 85 def test_my_account_should_show_language_select
86 86 get :account
87 87 assert_response :success
88 88 assert_select 'select[name=?]', 'user[language]'
89 89 end
90 90
91 91 def test_my_account_should_not_show_language_select_with_force_default_language_for_loggedin
92 92 with_settings :force_default_language_for_loggedin => '1' do
93 93 get :account
94 94 assert_response :success
95 95 assert_select 'select[name=?]', 'user[language]', 0
96 96 end
97 97 end
98 98
99 99 def test_update_account
100 100 post :account,
101 101 :user => {
102 102 :firstname => "Joe",
103 103 :login => "root",
104 104 :admin => 1,
105 105 :group_ids => ['10'],
106 106 :custom_field_values => {"4" => "0100562500"}
107 107 }
108 108
109 109 assert_redirected_to '/my/account'
110 110 user = User.find(2)
111 111 assert_equal user, assigns(:user)
112 112 assert_equal "Joe", user.firstname
113 113 assert_equal "jsmith", user.login
114 114 assert_equal "0100562500", user.custom_value_for(4).value
115 115 # ignored
116 116 assert !user.admin?
117 117 assert user.groups.empty?
118 118 end
119 119
120 120 def test_my_account_should_show_destroy_link
121 121 get :account
122 122 assert_select 'a[href="/my/account/destroy"]'
123 123 end
124 124
125 125 def test_get_destroy_should_display_the_destroy_confirmation
126 126 get :destroy
127 127 assert_response :success
128 128 assert_template 'destroy'
129 129 assert_select 'form[action="/my/account/destroy"]' do
130 130 assert_select 'input[name=confirm]'
131 131 end
132 132 end
133 133
134 134 def test_post_destroy_without_confirmation_should_not_destroy_account
135 135 assert_no_difference 'User.count' do
136 136 post :destroy
137 137 end
138 138 assert_response :success
139 139 assert_template 'destroy'
140 140 end
141 141
142 142 def test_post_destroy_without_confirmation_should_destroy_account
143 143 assert_difference 'User.count', -1 do
144 144 post :destroy, :confirm => '1'
145 145 end
146 146 assert_redirected_to '/'
147 147 assert_match /deleted/i, flash[:notice]
148 148 end
149 149
150 150 def test_post_destroy_with_unsubscribe_not_allowed_should_not_destroy_account
151 151 User.any_instance.stubs(:own_account_deletable?).returns(false)
152 152
153 153 assert_no_difference 'User.count' do
154 154 post :destroy, :confirm => '1'
155 155 end
156 156 assert_redirected_to '/my/account'
157 157 end
158 158
159 159 def test_change_password
160 160 get :password
161 161 assert_response :success
162 162 assert_template 'password'
163 163
164 164 # non matching password confirmation
165 165 post :password, :password => 'jsmith',
166 166 :new_password => 'secret123',
167 167 :new_password_confirmation => 'secret1234'
168 168 assert_response :success
169 169 assert_template 'password'
170 170 assert_select_error /Password doesn.*t match confirmation/
171 171
172 172 # wrong password
173 173 post :password, :password => 'wrongpassword',
174 174 :new_password => 'secret123',
175 175 :new_password_confirmation => 'secret123'
176 176 assert_response :success
177 177 assert_template 'password'
178 178 assert_equal 'Wrong password', flash[:error]
179 179
180 180 # good password
181 181 post :password, :password => 'jsmith',
182 182 :new_password => 'secret123',
183 183 :new_password_confirmation => 'secret123'
184 184 assert_redirected_to '/my/account'
185 185 assert User.try_to_login('jsmith', 'secret123')
186 186 end
187 187
188 def test_change_password_kills_other_sessions
189 @request.session[:ctime] = (Time.now - 30.minutes).utc.to_i
190
191 jsmith = User.find(2)
192 jsmith.passwd_changed_on = Time.now
193 jsmith.save!
194
195 get 'account'
196 assert_response 302
197 assert flash[:error].match(/Your session has expired/)
198 end
199
200 188 def test_change_password_should_redirect_if_user_cannot_change_its_password
201 189 User.find(2).update_attribute(:auth_source_id, 1)
202 190
203 191 get :password
204 192 assert_not_nil flash[:error]
205 193 assert_redirected_to '/my/account'
206 194 end
207 195
208 196 def test_page_layout
209 197 get :page_layout
210 198 assert_response :success
211 199 assert_template 'page_layout'
212 200 end
213 201
214 202 def test_add_block
215 203 post :add_block, :block => 'issuesreportedbyme'
216 204 assert_redirected_to '/my/page_layout'
217 205 assert User.find(2).pref[:my_page_layout]['top'].include?('issuesreportedbyme')
218 206 end
219 207
220 208 def test_add_invalid_block_should_redirect
221 209 post :add_block, :block => 'invalid'
222 210 assert_redirected_to '/my/page_layout'
223 211 end
224 212
225 213 def test_remove_block
226 214 post :remove_block, :block => 'issuesassignedtome'
227 215 assert_redirected_to '/my/page_layout'
228 216 assert !User.find(2).pref[:my_page_layout].values.flatten.include?('issuesassignedtome')
229 217 end
230 218
231 219 def test_order_blocks
232 220 xhr :post, :order_blocks, :group => 'left', 'blocks' => ['documents', 'calendar', 'latestnews']
233 221 assert_response :success
234 222 assert_equal ['documents', 'calendar', 'latestnews'], User.find(2).pref[:my_page_layout]['left']
235 223 end
236 224
237 225 def test_reset_rss_key_with_existing_key
238 226 @previous_token_value = User.find(2).rss_key # Will generate one if it's missing
239 227 post :reset_rss_key
240 228
241 229 assert_not_equal @previous_token_value, User.find(2).rss_key
242 230 assert User.find(2).rss_token
243 231 assert_match /reset/, flash[:notice]
244 232 assert_redirected_to '/my/account'
245 233 end
246 234
247 235 def test_reset_rss_key_without_existing_key
248 236 assert_nil User.find(2).rss_token
249 237 post :reset_rss_key
250 238
251 239 assert User.find(2).rss_token
252 240 assert_match /reset/, flash[:notice]
253 241 assert_redirected_to '/my/account'
254 242 end
255 243
256 244 def test_show_api_key
257 245 get :show_api_key
258 246 assert_response :success
259 247 assert_select 'pre', User.find(2).api_key
260 248 end
261 249
262 250 def test_reset_api_key_with_existing_key
263 251 @previous_token_value = User.find(2).api_key # Will generate one if it's missing
264 252 post :reset_api_key
265 253
266 254 assert_not_equal @previous_token_value, User.find(2).api_key
267 255 assert User.find(2).api_token
268 256 assert_match /reset/, flash[:notice]
269 257 assert_redirected_to '/my/account'
270 258 end
271 259
272 260 def test_reset_api_key_without_existing_key
273 261 assert_nil User.find(2).api_token
274 262 post :reset_api_key
275 263
276 264 assert User.find(2).api_token
277 265 assert_match /reset/, flash[:notice]
278 266 assert_redirected_to '/my/account'
279 267 end
280 268 end
@@ -1,132 +1,138
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 class SessionStartTest < ActionController::TestCase
21 tests AccountController
20 class SessionsControllerTest < ActionController::TestCase
21 include Redmine::I18n
22 tests WelcomeController
22 23
23 fixtures :users
24 fixtures :users, :email_addresses
24 25
25 def test_login_should_set_session_timestamps
26 post :login, :username => 'jsmith', :password => 'jsmith'
27 assert_response 302
28 assert_equal 2, session[:user_id]
29 assert_not_nil session[:ctime]
30 assert_not_nil session[:atime]
26 def setup
27 Rails.application.config.redmine_verify_sessions = true
31 28 end
32 end
33 29
34 class SessionsTest < ActionController::TestCase
35 include Redmine::I18n
36 tests WelcomeController
30 def teardown
31 Rails.application.config.redmine_verify_sessions = false
32 end
37 33
38 fixtures :users, :email_addresses
34 def test_session_token_should_be_updated
35 created = 10.hours.ago
36 token = Token.create!(:user_id => 2, :action => 'session', :created_on => created, :updated_on => created)
39 37
40 def test_atime_from_user_session_should_be_updated
41 created = 2.hours.ago.utc.to_i
42 get :index, {}, {:user_id => 2, :ctime => created, :atime => created}
38 get :index, {}, {:user_id => 2, :tk => token.value}
43 39 assert_response :success
44 assert_equal created, session[:ctime]
45 assert_not_equal created, session[:atime]
46 assert session[:atime] > created
40 token.reload
41 assert_equal created, token.created_on
42 assert_not_equal created, token.updated_on
43 assert token.updated_on > created
47 44 end
48 45
49 46 def test_user_session_should_not_be_reset_if_lifetime_and_timeout_disabled
47 created = 2.years.ago
48 token = Token.create!(:user_id => 2, :action => 'session', :created_on => created, :updated_on => created)
49
50 50 with_settings :session_lifetime => '0', :session_timeout => '0' do
51 get :index, {}, {:user_id => 2}
51 get :index, {}, {:user_id => 2, :tk => token.value}
52 52 assert_response :success
53 53 end
54 54 end
55 55
56 def test_user_session_without_ctime_should_be_reset_if_lifetime_enabled
57 with_settings :session_lifetime => '720' do
58 get :index, {}, {:user_id => 2}
59 assert_redirected_to 'http://test.host/login?back_url=http%3A%2F%2Ftest.host%2F'
60 end
56 def test_user_session_without_token_should_be_reset
57 get :index, {}, {:user_id => 2}
58 assert_redirected_to 'http://test.host/login?back_url=http%3A%2F%2Ftest.host%2F'
61 59 end
62 60
63 def test_user_session_with_expired_ctime_should_be_reset_if_lifetime_enabled
61 def test_expired_user_session_should_be_reset_if_lifetime_enabled
62 created = 2.days.ago
63 token = Token.create!(:user_id => 2, :action => 'session', :created_on => created, :updated_on => created)
64
64 65 with_settings :session_timeout => '720' do
65 get :index, {}, {:user_id => 2, :atime => 2.days.ago.utc.to_i}
66 get :index, {}, {:user_id => 2, :tk => token.value}
66 67 assert_redirected_to 'http://test.host/login?back_url=http%3A%2F%2Ftest.host%2F'
67 68 end
68 69 end
69 70
70 def test_user_session_with_valid_ctime_should_not_be_reset_if_lifetime_enabled
71 def test_valid_user_session_should_not_be_reset_if_lifetime_enabled
72 created = 3.hours.ago
73 token = Token.create!(:user_id => 2, :action => 'session', :created_on => created, :updated_on => created)
74
71 75 with_settings :session_timeout => '720' do
72 get :index, {}, {:user_id => 2, :atime => 3.hours.ago.utc.to_i}
76 get :index, {}, {:user_id => 2, :tk => token.value}
73 77 assert_response :success
74 78 end
75 79 end
76 80
77 def test_user_session_without_atime_should_be_reset_if_timeout_enabled
78 with_settings :session_timeout => '60' do
79 get :index, {}, {:user_id => 2}
80 assert_redirected_to 'http://test.host/login?back_url=http%3A%2F%2Ftest.host%2F'
81 end
82 end
81 def test_expired_user_session_should_be_reset_if_timeout_enabled
82 created = 4.hours.ago
83 token = Token.create!(:user_id => 2, :action => 'session', :created_on => created, :updated_on => created)
83 84
84 def test_user_session_with_expired_atime_should_be_reset_if_timeout_enabled
85 85 with_settings :session_timeout => '60' do
86 get :index, {}, {:user_id => 2, :atime => 4.hours.ago.utc.to_i}
86 get :index, {}, {:user_id => 2, :tk => token.value}
87 87 assert_redirected_to 'http://test.host/login?back_url=http%3A%2F%2Ftest.host%2F'
88 88 end
89 89 end
90 90
91 def test_user_session_with_valid_atime_should_not_be_reset_if_timeout_enabled
91 def test_valid_user_session_should_not_be_reset_if_timeout_enabled
92 created = 10.minutes.ago
93 token = Token.create!(:user_id => 2, :action => 'session', :created_on => created, :updated_on => created)
94
92 95 with_settings :session_timeout => '60' do
93 get :index, {}, {:user_id => 2, :atime => 10.minutes.ago.utc.to_i}
96 get :index, {}, {:user_id => 2, :tk => token.value}
94 97 assert_response :success
95 98 end
96 99 end
97 100
98 101 def test_expired_user_session_should_be_restarted_if_autologin
102 created = 2.hours.ago
103 token = Token.create!(:user_id => 2, :action => 'session', :created_on => created, :updated_on => created)
104
99 105 with_settings :session_lifetime => '720', :session_timeout => '60', :autologin => 7 do
100 token = Token.create!(:user_id => 2, :action => 'autologin', :created_on => 1.day.ago)
101 @request.cookies['autologin'] = token.value
102 created = 2.hours.ago.utc.to_i
106 autologin_token = Token.create!(:user_id => 2, :action => 'autologin', :created_on => 1.day.ago)
107 @request.cookies['autologin'] = autologin_token.value
103 108
104 get :index, {}, {:user_id => 2, :ctime => created, :atime => created}
109 get :index, {}, {:user_id => 2, :tk => token.value}
105 110 assert_equal 2, session[:user_id]
106 111 assert_response :success
107 assert_not_equal created, session[:ctime]
108 assert session[:ctime] >= created
112 assert_not_equal token.value, session[:tk]
109 113 end
110 114 end
111 115
112 116 def test_expired_user_session_should_set_locale
113 117 set_language_if_valid 'it'
114 118 user = User.find(2)
115 119 user.language = 'fr'
116 120 user.save!
121 created = 4.hours.ago
122 token = Token.create!(:user_id => 2, :action => 'session', :created_on => created, :updated_on => created)
117 123
118 124 with_settings :session_timeout => '60' do
119 get :index, {}, {:user_id => user.id, :atime => 4.hours.ago.utc.to_i}
125 get :index, {}, {:user_id => user.id, :tk => token.value}
120 126 assert_redirected_to 'http://test.host/login?back_url=http%3A%2F%2Ftest.host%2F'
121 127 assert_include "Veuillez vous reconnecter", flash[:error]
122 128 assert_equal :fr, current_language
123 129 end
124 130 end
125 131
126 132 def test_anonymous_session_should_not_be_reset
127 133 with_settings :session_lifetime => '720', :session_timeout => '60' do
128 134 get :index
129 135 assert_response :success
130 136 end
131 137 end
132 138 end
@@ -1,326 +1,338
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 AccountTest < Redmine::IntegrationTest
21 21 fixtures :users, :email_addresses, :roles
22 22
23 23 def test_login
24 24 get "/my/page"
25 25 assert_redirected_to "/login?back_url=http%3A%2F%2Fwww.example.com%2Fmy%2Fpage"
26 26 log_user('jsmith', 'jsmith')
27 27
28 28 get "/my/account"
29 29 assert_response :success
30 30 assert_template "my/account"
31 31 end
32 32
33 def test_login_should_set_session_token
34 assert_difference 'Token.count' do
35 log_user('jsmith', 'jsmith')
36
37 assert_equal 2, session[:user_id]
38 assert_not_nil session[:tk]
39 end
40 end
41
33 42 def test_autologin
34 43 user = User.find(1)
35 Setting.autologin = "7"
36 44 Token.delete_all
37 45
38 # User logs in with 'autologin' checked
39 post '/login', :username => user.login, :password => 'admin', :autologin => 1
40 assert_redirected_to '/my/page'
41 token = Token.first
42 assert_not_nil token
43 assert_equal user, token.user
44 assert_equal 'autologin', token.action
45 assert_equal user.id, session[:user_id]
46 assert_equal token.value, cookies['autologin']
47
48 # Session is cleared
49 reset!
50 User.current = nil
51 # Clears user's last login timestamp
52 user.update_attribute :last_login_on, nil
53 assert_nil user.reload.last_login_on
54
55 # User comes back with user's autologin cookie
56 cookies[:autologin] = token.value
57 get '/my/page'
58 assert_response :success
59 assert_template 'my/page'
60 assert_equal user.id, session[:user_id]
61 assert_not_nil user.reload.last_login_on
46 with_settings :autologin => '7' do
47 assert_difference 'Token.count', 2 do
48 # User logs in with 'autologin' checked
49 post '/login', :username => user.login, :password => 'admin', :autologin => 1
50 assert_redirected_to '/my/page'
51 end
52 token = Token.where(:action => 'autologin').order(:id => :desc).first
53 assert_not_nil token
54 assert_equal user, token.user
55 assert_equal 'autologin', token.action
56 assert_equal user.id, session[:user_id]
57 assert_equal token.value, cookies['autologin']
58
59 # Session is cleared
60 reset!
61 User.current = nil
62 # Clears user's last login timestamp
63 user.update_attribute :last_login_on, nil
64 assert_nil user.reload.last_login_on
65
66 # User comes back with user's autologin cookie
67 cookies[:autologin] = token.value
68 get '/my/page'
69 assert_response :success
70 assert_template 'my/page'
71 assert_equal user.id, session[:user_id]
72 assert_not_nil user.reload.last_login_on
73 end
62 74 end
63 75
64 76 def test_autologin_should_use_autologin_cookie_name
65 77 Token.delete_all
66 78 Redmine::Configuration.stubs(:[]).with('autologin_cookie_name').returns('custom_autologin')
67 79 Redmine::Configuration.stubs(:[]).with('autologin_cookie_path').returns('/')
68 80 Redmine::Configuration.stubs(:[]).with('autologin_cookie_secure').returns(false)
69 81 Redmine::Configuration.stubs(:[]).with('sudo_mode_timeout').returns(15)
70 82
71 83 with_settings :autologin => '7' do
72 assert_difference 'Token.count' do
84 assert_difference 'Token.count', 2 do
73 85 post '/login', :username => 'admin', :password => 'admin', :autologin => 1
74 86 assert_response 302
75 87 end
76 88 assert cookies['custom_autologin'].present?
77 89 token = cookies['custom_autologin']
78 90
79 91 # Session is cleared
80 92 reset!
81 93 cookies['custom_autologin'] = token
82 94 get '/my/page'
83 95 assert_response :success
84 96
85 assert_difference 'Token.count', -1 do
97 assert_difference 'Token.count', -2 do
86 98 post '/logout'
87 99 end
88 100 assert cookies['custom_autologin'].blank?
89 101 end
90 102 end
91 103
92 104 def test_lost_password
93 105 Token.delete_all
94 106
95 107 get "/account/lost_password"
96 108 assert_response :success
97 109 assert_template "account/lost_password"
98 110 assert_select 'input[name=mail]'
99 111
100 112 post "/account/lost_password", :mail => 'jSmith@somenet.foo'
101 113 assert_redirected_to "/login"
102 114
103 115 token = Token.first
104 116 assert_equal 'recovery', token.action
105 117 assert_equal 'jsmith@somenet.foo', token.user.mail
106 118 assert !token.expired?
107 119
108 120 get "/account/lost_password", :token => token.value
109 121 assert_response :success
110 122 assert_template "account/password_recovery"
111 123 assert_select 'input[type=hidden][name=token][value=?]', token.value
112 124 assert_select 'input[name=new_password]'
113 125 assert_select 'input[name=new_password_confirmation]'
114 126
115 127 post "/account/lost_password",
116 128 :token => token.value, :new_password => 'newpass123',
117 129 :new_password_confirmation => 'newpass123'
118 130 assert_redirected_to "/login"
119 131 assert_equal 'Password was successfully updated.', flash[:notice]
120 132
121 133 log_user('jsmith', 'newpass123')
122 assert_equal 0, Token.count
134 assert_equal false, Token.exists?(token.id), "Password recovery token was not deleted"
123 135 end
124 136
125 137 def test_user_with_must_change_passwd_should_be_forced_to_change_its_password
126 138 User.find_by_login('jsmith').update_attribute :must_change_passwd, true
127 139
128 140 post '/login', :username => 'jsmith', :password => 'jsmith'
129 141 assert_redirected_to '/my/page'
130 142 follow_redirect!
131 143 assert_redirected_to '/my/password'
132 144
133 145 get '/issues'
134 146 assert_redirected_to '/my/password'
135 147 end
136 148
137 149 def test_user_with_must_change_passwd_should_be_able_to_change_its_password
138 150 User.find_by_login('jsmith').update_attribute :must_change_passwd, true
139 151
140 152 post '/login', :username => 'jsmith', :password => 'jsmith'
141 153 assert_redirected_to '/my/page'
142 154 follow_redirect!
143 155 assert_redirected_to '/my/password'
144 156 follow_redirect!
145 157 assert_response :success
146 158 post '/my/password', :password => 'jsmith', :new_password => 'newpassword', :new_password_confirmation => 'newpassword'
147 159 assert_redirected_to '/my/account'
148 160 follow_redirect!
149 161 assert_response :success
150 162
151 163 assert_equal false, User.find_by_login('jsmith').must_change_passwd?
152 164 end
153 165
154 166 def test_user_with_expired_password_should_be_forced_to_change_its_password
155 167 User.find_by_login('jsmith').update_attribute :passwd_changed_on, 14.days.ago
156 168
157 169 with_settings :password_max_age => 7 do
158 170 post '/login', :username => 'jsmith', :password => 'jsmith'
159 171 assert_redirected_to '/my/page'
160 172 follow_redirect!
161 173 assert_redirected_to '/my/password'
162 174
163 175 get '/issues'
164 176 assert_redirected_to '/my/password'
165 177 end
166 178 end
167 179
168 180 def test_user_with_expired_password_should_be_able_to_change_its_password
169 181 User.find_by_login('jsmith').update_attribute :passwd_changed_on, 14.days.ago
170 182
171 183 with_settings :password_max_age => 7 do
172 184 post '/login', :username => 'jsmith', :password => 'jsmith'
173 185 assert_redirected_to '/my/page'
174 186 follow_redirect!
175 187 assert_redirected_to '/my/password'
176 188 follow_redirect!
177 189 assert_response :success
178 190 post '/my/password', :password => 'jsmith', :new_password => 'newpassword', :new_password_confirmation => 'newpassword'
179 191 assert_redirected_to '/my/account'
180 192 follow_redirect!
181 193 assert_response :success
182 194
183 195 assert_equal false, User.find_by_login('jsmith').must_change_passwd?
184 196 end
185 197
186 198 end
187 199
188 200 def test_register_with_automatic_activation
189 201 Setting.self_registration = '3'
190 202
191 203 get '/account/register'
192 204 assert_response :success
193 205 assert_template 'account/register'
194 206
195 207 post '/account/register',
196 208 :user => {:login => "newuser", :language => "en",
197 209 :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar",
198 210 :password => "newpass123", :password_confirmation => "newpass123"}
199 211 assert_redirected_to '/my/account'
200 212 follow_redirect!
201 213 assert_response :success
202 214 assert_template 'my/account'
203 215
204 216 user = User.find_by_login('newuser')
205 217 assert_not_nil user
206 218 assert user.active?
207 219 assert_not_nil user.last_login_on
208 220 end
209 221
210 222 def test_register_with_manual_activation
211 223 Setting.self_registration = '2'
212 224
213 225 post '/account/register',
214 226 :user => {:login => "newuser", :language => "en",
215 227 :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar",
216 228 :password => "newpass123", :password_confirmation => "newpass123"}
217 229 assert_redirected_to '/login'
218 230 assert !User.find_by_login('newuser').active?
219 231 end
220 232
221 233 def test_register_with_email_activation
222 234 Setting.self_registration = '1'
223 235 Token.delete_all
224 236
225 237 post '/account/register',
226 238 :user => {:login => "newuser", :language => "en",
227 239 :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar",
228 240 :password => "newpass123", :password_confirmation => "newpass123"}
229 241 assert_redirected_to '/login'
230 242 assert !User.find_by_login('newuser').active?
231 243
232 244 token = Token.first
233 245 assert_equal 'register', token.action
234 246 assert_equal 'newuser@foo.bar', token.user.mail
235 247 assert !token.expired?
236 248
237 249 get '/account/activate', :token => token.value
238 250 assert_redirected_to '/login'
239 251 log_user('newuser', 'newpass123')
240 252 end
241 253
242 254 def test_onthefly_registration
243 255 # disable registration
244 256 Setting.self_registration = '0'
245 257 AuthSource.expects(:authenticate).returns(
246 258 {:login => 'foo', :firstname => 'Foo', :lastname => 'Smith',
247 259 :mail => 'foo@bar.com', :auth_source_id => 66})
248 260
249 261 post '/login', :username => 'foo', :password => 'bar'
250 262 assert_redirected_to '/my/page'
251 263
252 264 user = User.find_by_login('foo')
253 265 assert user.is_a?(User)
254 266 assert_equal 66, user.auth_source_id
255 267 assert user.hashed_password.blank?
256 268 end
257 269
258 270 def test_onthefly_registration_with_invalid_attributes
259 271 # disable registration
260 272 Setting.self_registration = '0'
261 273 AuthSource.expects(:authenticate).returns(
262 274 {:login => 'foo', :lastname => 'Smith', :auth_source_id => 66})
263 275
264 276 post '/login', :username => 'foo', :password => 'bar'
265 277 assert_response :success
266 278 assert_template 'account/register'
267 279 assert_select 'input[name=?][value=""]', 'user[firstname]'
268 280 assert_select 'input[name=?][value=Smith]', 'user[lastname]'
269 281 assert_select 'input[name=?]', 'user[login]', 0
270 282 assert_select 'input[name=?]', 'user[password]', 0
271 283
272 284 post '/account/register',
273 285 :user => {:firstname => 'Foo', :lastname => 'Smith', :mail => 'foo@bar.com'}
274 286 assert_redirected_to '/my/account'
275 287
276 288 user = User.find_by_login('foo')
277 289 assert user.is_a?(User)
278 290 assert_equal 66, user.auth_source_id
279 291 assert user.hashed_password.blank?
280 292 end
281 293
282 294 def test_registered_user_should_be_able_to_get_a_new_activation_email
283 295 Token.delete_all
284 296
285 297 with_settings :self_registration => '1', :default_language => 'en' do
286 298 # register a new account
287 299 assert_difference 'User.count' do
288 300 assert_difference 'Token.count' do
289 301 post '/account/register',
290 302 :user => {:login => "newuser", :language => "en",
291 303 :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar",
292 304 :password => "newpass123", :password_confirmation => "newpass123"}
293 305 end
294 306 end
295 307 user = User.order('id desc').first
296 308 assert_equal User::STATUS_REGISTERED, user.status
297 309 reset!
298 310
299 311 # try to use "lost password"
300 312 assert_no_difference 'ActionMailer::Base.deliveries.size' do
301 313 post '/account/lost_password', :mail => 'newuser@foo.bar'
302 314 end
303 315 assert_redirected_to '/account/lost_password'
304 316 follow_redirect!
305 317 assert_response :success
306 318 assert_select 'div.flash', :text => /new activation email/
307 319 assert_select 'div.flash a[href="/account/activation_email"]'
308 320
309 321 # request a new action activation email
310 322 assert_difference 'ActionMailer::Base.deliveries.size' do
311 323 get '/account/activation_email'
312 324 end
313 325 assert_redirected_to '/login'
314 326 token = Token.order('id desc').first
315 327 activation_path = "/account/activate?token=#{token.value}"
316 328 assert_include activation_path, mail_body(ActionMailer::Base.deliveries.last)
317 329
318 330 # activate the account
319 331 get activation_path
320 332 assert_redirected_to '/login'
321 333
322 334 post '/login', :username => 'newuser', :password => 'newpass123'
323 335 assert_redirected_to '/my/page'
324 336 end
325 337 end
326 338 end
@@ -1,108 +1,121
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 TokenTest < ActiveSupport::TestCase
21 21 fixtures :tokens
22 22
23 23 def test_create
24 24 token = Token.new
25 25 token.save
26 26 assert_equal 40, token.value.length
27 27 assert !token.expired?
28 28 end
29 29
30 30 def test_create_should_remove_existing_tokens
31 31 user = User.find(1)
32 32 t1 = Token.create(:user => user, :action => 'autologin')
33 33 t2 = Token.create(:user => user, :action => 'autologin')
34 34 assert_not_equal t1.value, t2.value
35 35 assert !Token.exists?(t1.id)
36 36 assert Token.exists?(t2.id)
37 37 end
38 38
39 def test_create_session_token_should_keep_last_10_tokens
40 Token.delete_all
41 user = User.find(1)
42
43 assert_difference 'Token.count', 10 do
44 10.times { Token.create!(:user => user, :action => 'session') }
45 end
46
47 assert_no_difference 'Token.count' do
48 Token.create!(:user => user, :action => 'session')
49 end
50 end
51
39 52 def test_destroy_expired_should_not_destroy_feeds_and_api_tokens
40 53 Token.delete_all
41 54
42 55 Token.create!(:user_id => 1, :action => 'api', :created_on => 7.days.ago)
43 56 Token.create!(:user_id => 1, :action => 'feeds', :created_on => 7.days.ago)
44 57
45 58 assert_no_difference 'Token.count' do
46 59 assert_equal 0, Token.destroy_expired
47 60 end
48 61 end
49 62
50 63 def test_destroy_expired_should_destroy_expired_tokens
51 64 Token.delete_all
52 65
53 66 Token.create!(:user_id => 1, :action => 'autologin', :created_on => 7.days.ago)
54 67 Token.create!(:user_id => 2, :action => 'autologin', :created_on => 3.days.ago)
55 68 Token.create!(:user_id => 3, :action => 'autologin', :created_on => 1.hour.ago)
56 69
57 70 assert_difference 'Token.count', -2 do
58 71 assert_equal 2, Token.destroy_expired
59 72 end
60 73 end
61 74
62 75 def test_find_active_user_should_return_user
63 76 token = Token.create!(:user_id => 1, :action => 'api')
64 77 assert_equal User.find(1), Token.find_active_user('api', token.value)
65 78 end
66 79
67 80 def test_find_active_user_should_return_nil_for_locked_user
68 81 token = Token.create!(:user_id => 1, :action => 'api')
69 82 User.find(1).lock!
70 83 assert_nil Token.find_active_user('api', token.value)
71 84 end
72 85
73 86 def test_find_user_should_return_user
74 87 token = Token.create!(:user_id => 1, :action => 'api')
75 88 assert_equal User.find(1), Token.find_user('api', token.value)
76 89 end
77 90
78 91 def test_find_user_should_return_locked_user
79 92 token = Token.create!(:user_id => 1, :action => 'api')
80 93 User.find(1).lock!
81 94 assert_equal User.find(1), Token.find_user('api', token.value)
82 95 end
83 96
84 97 def test_find_token_should_return_the_token
85 98 token = Token.create!(:user_id => 1, :action => 'api')
86 99 assert_equal token, Token.find_token('api', token.value)
87 100 end
88 101
89 102 def test_find_token_should_return_the_token_with_validity
90 103 token = Token.create!(:user_id => 1, :action => 'api', :created_on => 1.hour.ago)
91 104 assert_equal token, Token.find_token('api', token.value, 1)
92 105 end
93 106
94 107 def test_find_token_should_return_nil_with_wrong_action
95 108 token = Token.create!(:user_id => 1, :action => 'feeds')
96 109 assert_nil Token.find_token('api', token.value)
97 110 end
98 111
99 112 def test_find_token_should_return_nil_without_user
100 113 token = Token.create!(:user_id => 999, :action => 'api')
101 114 assert_nil Token.find_token('api', token.value)
102 115 end
103 116
104 117 def test_find_token_should_return_nil_with_validity_expired
105 118 token = Token.create!(:user_id => 999, :action => 'api', :created_on => 2.days.ago)
106 119 assert_nil Token.find_token('api', token.value, 1)
107 120 end
108 121 end
General Comments 0
You need to be logged in to leave comments. Login now