##// END OF EJS Templates
Remove autologin cookie on unverified request....
Jean-Philippe Lang -
r6196:b81149fa47ed
parent child
Show More
@@ -1,518 +1,521
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require '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
26 26 layout 'base'
27 27 exempt_from_layout 'builder', 'rsb'
28 28
29 29 protect_from_forgery
30
30 def handle_unverified_request
31 super
32 cookies.delete(:autologin)
33 end
31 34 # Remove broken cookie after upgrade from 0.8.x (#4292)
32 35 # See https://rails.lighthouseapp.com/projects/8994/tickets/3360
33 36 # TODO: remove it when Rails is fixed
34 37 before_filter :delete_broken_cookies
35 38 def delete_broken_cookies
36 39 if cookies['_redmine_session'] && cookies['_redmine_session'] !~ /--/
37 40 cookies.delete '_redmine_session'
38 41 redirect_to home_path
39 42 return false
40 43 end
41 44 end
42 45
43 46 before_filter :user_setup, :check_if_login_required, :set_localization
44 47 filter_parameter_logging :password
45 48
46 49 rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
47 50 rescue_from ::Unauthorized, :with => :deny_access
48 51
49 52 include Redmine::Search::Controller
50 53 include Redmine::MenuManager::MenuController
51 54 helper Redmine::MenuManager::MenuHelper
52 55
53 56 Redmine::Scm::Base.all.each do |scm|
54 57 require_dependency "repository/#{scm.underscore}"
55 58 end
56 59
57 60 def user_setup
58 61 # Check the settings cache for each request
59 62 Setting.check_cache
60 63 # Find the current user
61 64 User.current = find_current_user
62 65 end
63 66
64 67 # Returns the current user or nil if no user is logged in
65 68 # and starts a session if needed
66 69 def find_current_user
67 70 if session[:user_id]
68 71 # existing session
69 72 (User.active.find(session[:user_id]) rescue nil)
70 73 elsif cookies[:autologin] && Setting.autologin?
71 74 # auto-login feature starts a new session
72 75 user = User.try_to_autologin(cookies[:autologin])
73 76 session[:user_id] = user.id if user
74 77 user
75 78 elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth?
76 79 # RSS key authentication does not start a session
77 80 User.find_by_rss_key(params[:key])
78 81 elsif Setting.rest_api_enabled? && accept_api_auth?
79 82 if (key = api_key_from_request)
80 83 # Use API key
81 84 User.find_by_api_key(key)
82 85 else
83 86 # HTTP Basic, either username/password or API key/random
84 87 authenticate_with_http_basic do |username, password|
85 88 User.try_to_login(username, password) || User.find_by_api_key(username)
86 89 end
87 90 end
88 91 end
89 92 end
90 93
91 94 # Sets the logged in user
92 95 def logged_user=(user)
93 96 reset_session
94 97 if user && user.is_a?(User)
95 98 User.current = user
96 99 session[:user_id] = user.id
97 100 else
98 101 User.current = User.anonymous
99 102 end
100 103 end
101 104
102 105 # check if login is globally required to access the application
103 106 def check_if_login_required
104 107 # no check needed if user is already logged in
105 108 return true if User.current.logged?
106 109 require_login if Setting.login_required?
107 110 end
108 111
109 112 def set_localization
110 113 lang = nil
111 114 if User.current.logged?
112 115 lang = find_language(User.current.language)
113 116 end
114 117 if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
115 118 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
116 119 if !accept_lang.blank?
117 120 accept_lang = accept_lang.downcase
118 121 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
119 122 end
120 123 end
121 124 lang ||= Setting.default_language
122 125 set_language_if_valid(lang)
123 126 end
124 127
125 128 def require_login
126 129 if !User.current.logged?
127 130 # Extract only the basic url parameters on non-GET requests
128 131 if request.get?
129 132 url = url_for(params)
130 133 else
131 134 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
132 135 end
133 136 respond_to do |format|
134 137 format.html { redirect_to :controller => "account", :action => "login", :back_url => url }
135 138 format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
136 139 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
137 140 format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
138 141 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
139 142 end
140 143 return false
141 144 end
142 145 true
143 146 end
144 147
145 148 def require_admin
146 149 return unless require_login
147 150 if !User.current.admin?
148 151 render_403
149 152 return false
150 153 end
151 154 true
152 155 end
153 156
154 157 def deny_access
155 158 User.current.logged? ? render_403 : require_login
156 159 end
157 160
158 161 # Authorize the user for the requested action
159 162 def authorize(ctrl = params[:controller], action = params[:action], global = false)
160 163 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
161 164 if allowed
162 165 true
163 166 else
164 167 if @project && @project.archived?
165 168 render_403 :message => :notice_not_authorized_archived_project
166 169 else
167 170 deny_access
168 171 end
169 172 end
170 173 end
171 174
172 175 # Authorize the user for the requested action outside a project
173 176 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
174 177 authorize(ctrl, action, global)
175 178 end
176 179
177 180 # Find project of id params[:id]
178 181 def find_project
179 182 @project = Project.find(params[:id])
180 183 rescue ActiveRecord::RecordNotFound
181 184 render_404
182 185 end
183 186
184 187 # Find project of id params[:project_id]
185 188 def find_project_by_project_id
186 189 @project = Project.find(params[:project_id])
187 190 rescue ActiveRecord::RecordNotFound
188 191 render_404
189 192 end
190 193
191 194 # Find a project based on params[:project_id]
192 195 # TODO: some subclasses override this, see about merging their logic
193 196 def find_optional_project
194 197 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
195 198 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
196 199 allowed ? true : deny_access
197 200 rescue ActiveRecord::RecordNotFound
198 201 render_404
199 202 end
200 203
201 204 # Finds and sets @project based on @object.project
202 205 def find_project_from_association
203 206 render_404 unless @object.present?
204 207
205 208 @project = @object.project
206 209 rescue ActiveRecord::RecordNotFound
207 210 render_404
208 211 end
209 212
210 213 def find_model_object
211 214 model = self.class.read_inheritable_attribute('model_object')
212 215 if model
213 216 @object = model.find(params[:id])
214 217 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
215 218 end
216 219 rescue ActiveRecord::RecordNotFound
217 220 render_404
218 221 end
219 222
220 223 def self.model_object(model)
221 224 write_inheritable_attribute('model_object', model)
222 225 end
223 226
224 227 # Filter for bulk issue operations
225 228 def find_issues
226 229 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
227 230 raise ActiveRecord::RecordNotFound if @issues.empty?
228 231 if @issues.detect {|issue| !issue.visible?}
229 232 deny_access
230 233 return
231 234 end
232 235 @projects = @issues.collect(&:project).compact.uniq
233 236 @project = @projects.first if @projects.size == 1
234 237 rescue ActiveRecord::RecordNotFound
235 238 render_404
236 239 end
237 240
238 241 # Check if project is unique before bulk operations
239 242 def check_project_uniqueness
240 243 unless @project
241 244 # TODO: let users bulk edit/move/destroy issues from different projects
242 245 render_error 'Can not bulk edit/move/destroy issues from different projects'
243 246 return false
244 247 end
245 248 end
246 249
247 250 # make sure that the user is a member of the project (or admin) if project is private
248 251 # used as a before_filter for actions that do not require any particular permission on the project
249 252 def check_project_privacy
250 253 if @project && @project.active?
251 254 if @project.is_public? || User.current.member_of?(@project) || User.current.admin?
252 255 true
253 256 else
254 257 User.current.logged? ? render_403 : require_login
255 258 end
256 259 else
257 260 @project = nil
258 261 render_404
259 262 false
260 263 end
261 264 end
262 265
263 266 def back_url
264 267 params[:back_url] || request.env['HTTP_REFERER']
265 268 end
266 269
267 270 def redirect_back_or_default(default)
268 271 back_url = CGI.unescape(params[:back_url].to_s)
269 272 if !back_url.blank?
270 273 begin
271 274 uri = URI.parse(back_url)
272 275 # do not redirect user to another host or to the login or register page
273 276 if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
274 277 redirect_to(back_url)
275 278 return
276 279 end
277 280 rescue URI::InvalidURIError
278 281 # redirect to default
279 282 end
280 283 end
281 284 redirect_to default
282 285 false
283 286 end
284 287
285 288 def render_403(options={})
286 289 @project = nil
287 290 render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
288 291 return false
289 292 end
290 293
291 294 def render_404(options={})
292 295 render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
293 296 return false
294 297 end
295 298
296 299 # Renders an error response
297 300 def render_error(arg)
298 301 arg = {:message => arg} unless arg.is_a?(Hash)
299 302
300 303 @message = arg[:message]
301 304 @message = l(@message) if @message.is_a?(Symbol)
302 305 @status = arg[:status] || 500
303 306
304 307 respond_to do |format|
305 308 format.html {
306 309 render :template => 'common/error', :layout => use_layout, :status => @status
307 310 }
308 311 format.atom { head @status }
309 312 format.xml { head @status }
310 313 format.js { head @status }
311 314 format.json { head @status }
312 315 end
313 316 end
314 317
315 318 # Picks which layout to use based on the request
316 319 #
317 320 # @return [boolean, string] name of the layout to use or false for no layout
318 321 def use_layout
319 322 request.xhr? ? false : 'base'
320 323 end
321 324
322 325 def invalid_authenticity_token
323 326 if api_request?
324 327 logger.error "Form authenticity token is missing or is invalid. API calls must include a proper Content-type header (text/xml or text/json)."
325 328 end
326 329 render_error "Invalid form authenticity token."
327 330 end
328 331
329 332 def render_feed(items, options={})
330 333 @items = items || []
331 334 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
332 335 @items = @items.slice(0, Setting.feeds_limit.to_i)
333 336 @title = options[:title] || Setting.app_title
334 337 render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
335 338 end
336 339
337 340 # TODO: remove in Redmine 1.4
338 341 def self.accept_key_auth(*actions)
339 342 ActiveSupport::Deprecation.warn "ApplicationController.accept_key_auth is deprecated and will be removed in Redmine 1.4. Use accept_rss_auth (or accept_api_auth) instead."
340 343 accept_rss_auth(*actions)
341 344 end
342 345
343 346 # TODO: remove in Redmine 1.4
344 347 def accept_key_auth_actions
345 348 ActiveSupport::Deprecation.warn "ApplicationController.accept_key_auth_actions is deprecated and will be removed in Redmine 1.4. Use accept_rss_auth (or accept_api_auth) instead."
346 349 self.class.accept_rss_auth
347 350 end
348 351
349 352 def self.accept_rss_auth(*actions)
350 353 if actions.any?
351 354 write_inheritable_attribute('accept_rss_auth_actions', actions)
352 355 else
353 356 read_inheritable_attribute('accept_rss_auth_actions') || []
354 357 end
355 358 end
356 359
357 360 def accept_rss_auth?(action=action_name)
358 361 self.class.accept_rss_auth.include?(action.to_sym)
359 362 end
360 363
361 364 def self.accept_api_auth(*actions)
362 365 if actions.any?
363 366 write_inheritable_attribute('accept_api_auth_actions', actions)
364 367 else
365 368 read_inheritable_attribute('accept_api_auth_actions') || []
366 369 end
367 370 end
368 371
369 372 def accept_api_auth?(action=action_name)
370 373 self.class.accept_api_auth.include?(action.to_sym)
371 374 end
372 375
373 376 # Returns the number of objects that should be displayed
374 377 # on the paginated list
375 378 def per_page_option
376 379 per_page = nil
377 380 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
378 381 per_page = params[:per_page].to_s.to_i
379 382 session[:per_page] = per_page
380 383 elsif session[:per_page]
381 384 per_page = session[:per_page]
382 385 else
383 386 per_page = Setting.per_page_options_array.first || 25
384 387 end
385 388 per_page
386 389 end
387 390
388 391 # Returns offset and limit used to retrieve objects
389 392 # for an API response based on offset, limit and page parameters
390 393 def api_offset_and_limit(options=params)
391 394 if options[:offset].present?
392 395 offset = options[:offset].to_i
393 396 if offset < 0
394 397 offset = 0
395 398 end
396 399 end
397 400 limit = options[:limit].to_i
398 401 if limit < 1
399 402 limit = 25
400 403 elsif limit > 100
401 404 limit = 100
402 405 end
403 406 if offset.nil? && options[:page].present?
404 407 offset = (options[:page].to_i - 1) * limit
405 408 offset = 0 if offset < 0
406 409 end
407 410 offset ||= 0
408 411
409 412 [offset, limit]
410 413 end
411 414
412 415 # qvalues http header parser
413 416 # code taken from webrick
414 417 def parse_qvalues(value)
415 418 tmp = []
416 419 if value
417 420 parts = value.split(/,\s*/)
418 421 parts.each {|part|
419 422 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
420 423 val = m[1]
421 424 q = (m[2] or 1).to_f
422 425 tmp.push([val, q])
423 426 end
424 427 }
425 428 tmp = tmp.sort_by{|val, q| -q}
426 429 tmp.collect!{|val, q| val}
427 430 end
428 431 return tmp
429 432 rescue
430 433 nil
431 434 end
432 435
433 436 # Returns a string that can be used as filename value in Content-Disposition header
434 437 def filename_for_content_disposition(name)
435 438 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
436 439 end
437 440
438 441 def api_request?
439 442 %w(xml json).include? params[:format]
440 443 end
441 444
442 445 # Returns the API key present in the request
443 446 def api_key_from_request
444 447 if params[:key].present?
445 448 params[:key]
446 449 elsif request.headers["X-Redmine-API-Key"].present?
447 450 request.headers["X-Redmine-API-Key"]
448 451 end
449 452 end
450 453
451 454 # Renders a warning flash if obj has unsaved attachments
452 455 def render_attachment_warning_if_needed(obj)
453 456 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
454 457 end
455 458
456 459 # Sets the `flash` notice or error based the number of issues that did not save
457 460 #
458 461 # @param [Array, Issue] issues all of the saved and unsaved Issues
459 462 # @param [Array, Integer] unsaved_issue_ids the issue ids that were not saved
460 463 def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
461 464 if unsaved_issue_ids.empty?
462 465 flash[:notice] = l(:notice_successful_update) unless issues.empty?
463 466 else
464 467 flash[:error] = l(:notice_failed_to_save_issues,
465 468 :count => unsaved_issue_ids.size,
466 469 :total => issues.size,
467 470 :ids => '#' + unsaved_issue_ids.join(', #'))
468 471 end
469 472 end
470 473
471 474 # Rescues an invalid query statement. Just in case...
472 475 def query_statement_invalid(exception)
473 476 logger.error "Query::StatementInvalid: #{exception.message}" if logger
474 477 session.delete(:query)
475 478 sort_clear if respond_to?(:sort_clear)
476 479 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
477 480 end
478 481
479 482 # Converts the errors on an ActiveRecord object into a common JSON format
480 483 def object_errors_to_json(object)
481 484 object.errors.collect do |attribute, error|
482 485 { attribute => error }
483 486 end.to_json
484 487 end
485 488
486 489 # Renders API response on validation failure
487 490 def render_validation_errors(object)
488 491 options = { :status => :unprocessable_entity, :layout => false }
489 492 options.merge!(case params[:format]
490 493 when 'xml'; { :xml => object.errors }
491 494 when 'json'; { :json => {'errors' => object.errors} } # ActiveResource client compliance
492 495 else
493 496 raise "Unknown format #{params[:format]} in #render_validation_errors"
494 497 end
495 498 )
496 499 render options
497 500 end
498 501
499 502 # Overrides #default_template so that the api template
500 503 # is used automatically if it exists
501 504 def default_template(action_name = self.action_name)
502 505 if api_request?
503 506 begin
504 507 return self.view_paths.find_template(default_template_name(action_name), 'api')
505 508 rescue ::ActionView::MissingTemplate
506 509 # the api template was not found
507 510 # fallback to the default behaviour
508 511 end
509 512 end
510 513 super
511 514 end
512 515
513 516 # Overrides #pick_layout so that #render with no arguments
514 517 # doesn't use the layout for api requests
515 518 def pick_layout(*args)
516 519 api_request? ? nil : super
517 520 end
518 521 end
General Comments 0
You need to be logged in to leave comments. Login now