Files

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

461 lines
15 KiB
Ruby
Raw Permalink Normal View History

# frozen_string_literal: true
#-- copyright
2020-01-15 11:31:26 +01:00
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
2011-05-30 20:52:25 +02:00
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
2011-05-30 20:52:25 +02:00
#
2013-09-16 17:59:31 +02:00
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
2021-01-13 17:47:45 +01:00
# Copyright (C) 2006-2013 Jean-Philippe Lang
2013-09-16 17:59:31 +02:00
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
require "uri"
require "cgi"
2020-12-13 14:59:24 +01:00
require "doorkeeper/dashboard_helper"
class ApplicationController < ActionController::Base
class_attribute :_model_scope
2012-08-31 13:56:33 +02:00
class_attribute :accept_key_auth_actions
helper_method :render_to_string
2022-02-09 14:59:04 +01:00
helper_method :gon
protected
2011-05-30 20:52:25 +02:00
2016-03-14 10:55:33 +01:00
include I18n
include Redmine::I18n
2017-02-15 21:00:48 +01:00
include HookHelper
include ErrorsHelper
2020-05-11 11:05:33 +02:00
include Accounts::CurrentUser
include Accounts::UserLogin
include Accounts::Authorization
include Accounts::EnterpriseGuard
include ::OpenProject::Authentication::SessionExpiration
include AdditionalUrlHelpers
include OpenProjectErrorHelper
2024-06-25 08:42:12 +02:00
include Security::DefaultUrlOptions
include OpModalFlashable
2025-07-02 15:18:27 +02:00
include DynamicContentSecurityPolicy
2009-09-13 17:14:35 +00:00
layout "base"
2011-05-30 20:52:25 +02:00
protect_from_forgery
2014-05-14 09:53:25 +02:00
# CSRF protection prevents two things. It prevents an attacker from using a
# user's session to execute requests. It also prevents an attacker to log in
# a user with the attacker's account. API requests each contain their own
# authentication token, e.g. as key parameter or header, so they don't have
# to be protected by CSRF protection as long as they don't create a session
#
# We can't reliably determine here whether a request is an API
# request as this happens in our way too complex find_current_user method
# that is only executed after this method. E.g we might have to check that
# no session is active and that no autologin cookie is set.
#
# Thus, we always reset any active session and the autologin cookie to make
# sure find_current user doesn't find a user based on an active session.
2014-05-14 09:53:25 +02:00
#
# Nevertheless, API requests should not be aborted, which they would be
# if we raised an error here. Still, users should see an error message
# when sending a form with a wrong CSRF token (e.g. after session expiration).
# Thus, we show an error message unless the request probably is an API
# request.
def handle_unverified_request
2014-04-07 16:05:04 +02:00
cookies.delete(OpenProject::Configuration["autologin_cookie_name"])
self.logged_user = nil
# Don't render an error message for requests that appear to be API requests.
#
# The api_request? method uses the format parameter or a header
# to determine whether a request is an API request. Unfortunately, having
# an API request doesn't mean we don't use a session for authentication.
# Also, attackers can send CSRF requests with arbitrary headers using
# browser plugins. For more information on this, see:
# http://weblog.rubyonrails.org/2011/2/8/csrf-protection-bypass-in-ruby-on-rails/
2014-05-14 09:53:25 +02:00
#
# Resetting the session above is enough for preventing an attacking from
# using a user's session to execute requests with the user's account.
#
# It's not enough to prevent login CSRF, so we have to explicitly deny requests
# with invalid CSRF token for all requests that create a session with a logged in
# user. This is implemented as a before filter on AccountController that disallows
# all requests classified as API calls by api_request (via disable_api). It's
# important that disable_api and handle_unverified_request both use the same method
# to determine whether a request is an API request to ensure that a request either
# has a valid CSRF token and is not classified as API request, so no error is raised
# here OR a request has an invalid CSRF token and is classified as API request, no error
# is raised here, but is denied by disable_api.
#
# See http://stackoverflow.com/a/15350123 for more information on login CSRF.
2016-02-12 19:07:37 +01:00
unless api_request?
# Check whether user have cookies enabled, otherwise they'll only be
# greeted with the CSRF error upon login.
message = I18n.t(:error_token_authenticity)
2016-02-15 09:41:14 +01:00
message << (" " + I18n.t(:error_cookie_missing)) if openproject_cookie_missing?
2019-01-22 14:45:25 +01:00
log_csrf_failure
2016-02-12 19:07:37 +01:00
render_error status: 422, message:
end
end
# Ensure the default handler is listed FIRST
unless Rails.application.config.consider_all_requests_local
rescue_from StandardError do |exception|
render_500 exception:
end
rescue_from ActionController::UnknownFormat do
render body: "406 Not Acceptable: invalid request format",
status: :not_acceptable
end
end
rescue_from ActionController::ParameterMissing do |exception|
2021-02-11 16:02:18 +01:00
render body: "Required parameter missing: #{exception.param}",
status: :bad_request
end
rescue_from ActiveRecord::ConnectionTimeoutError do |exception|
render_500 exception:,
payload: ::OpenProject::Logging::ThreadPoolContextBuilder.build!
end
rescue_from ActiveRecord::RecordNotFound do
render_404
end
before_action :authorization_check_required,
:user_setup,
:set_localization,
2022-06-09 16:20:51 +02:00
:tag_request,
2013-08-29 17:31:05 +01:00
:check_if_login_required,
:log_requesting_user,
2013-09-10 10:03:55 +02:00
:check_session_lifetime,
:stop_if_feeds_disabled,
:set_cache_buster,
2018-03-21 08:59:59 +01:00
:action_hooks,
:reload_mailer_settings!
2009-10-21 17:07:18 +00:00
include Redmine::Search::Controller
include Redmine::MenuManager::MenuController
2025-09-17 15:22:29 +02:00
helper Redmine::MenuManager::MenuHelper
2011-05-30 20:52:25 +02:00
# set http headers so that the browser does not store any
# data (caches) of this site
# see:
# https://websecuritytool.codeplex.com/wikipage?title=Checks#http-cache-control-header-no-store
# http://stackoverflow.com/questions/711418/how-to-prevent-browser-page-caching-in-rails
def set_cache_buster
if OpenProject::Configuration["disable_browser_cache"]
2017-01-17 16:40:58 +01:00
response.cache_control.merge!(
max_age: 0,
public: false,
must_revalidate: true
)
end
end
2022-06-09 16:20:51 +02:00
def tag_request
2025-09-17 15:22:29 +02:00
context = { controller: self, request: }
::OpenProject::Appsignal.tag_request(context)
::OpenProject::OpenTelemetry.tag_request(context)
2022-06-09 16:20:51 +02:00
end
def reload_mailer_settings!
Setting.reload_mailer_settings!
end
2016-02-15 09:41:14 +01:00
# Checks if the session cookie is missing.
# This is useful only on a second request
def openproject_cookie_missing?
request.cookies[OpenProject::Configuration["session_cookie_name"]].nil?
end
2016-02-15 09:41:14 +01:00
helper_method :openproject_cookie_missing?
2019-01-22 14:45:25 +01:00
##
# Create CSRF issue
def log_csrf_failure
message = "CSRF validation error"
message += " (No session cookie present)" if openproject_cookie_missing?
2019-01-22 14:45:25 +01:00
op_handle_error message, reference: :csrf_validation_failed
end
2013-08-29 17:31:05 +01:00
def log_requesting_user
return unless Setting.log_requesting_user?
2021-02-11 16:02:18 +01:00
unless User.current.anonymous?
login_and_mail = " (#{escape_for_logging(User.current.login)} ID: #{User.current.id} " \
"<#{escape_for_logging(User.current.mail)}>)"
end
2013-08-29 17:31:05 +01:00
logger.info "OpenProject User: #{escape_for_logging(User.current.name)}#{login_and_mail}"
end
# Escape string to prevent log injection
# e.g. setting the user name to contain \r allows overwriting a log line on console
# replaces all invalid characters with #
def escape_for_logging(string)
# only allow numbers, ASCII letters, space and the following characters: @.-"'!?=/
2021-02-11 16:02:18 +01:00
string.gsub(/[^0-9a-zA-Z@._\-"'!?=\/ ]{1}/, "#")
2013-08-29 17:31:05 +01:00
end
def set_localization
2024-06-07 16:56:41 +02:00
# 1. Use completely authenticated user
# 2. Use user with some authenticated stages not completed.
# In this case user is not considered logged in, but identified.
# It covers localization for extra authentication stages(like :consent, for example)
# 3. Use anonymous instance.
user = RequestStore[:current_user] ||
(session[:authenticated_user_id].present? && User.find_by(id: session[:authenticated_user_id])) ||
User.anonymous
SetLocalizationService.new(user, request.env["HTTP_ACCEPT_LANGUAGE"]).call
end
2011-05-30 20:52:25 +02:00
2019-09-04 11:21:18 +02:00
def deny_access(not_found: false)
if User.current.logged?
not_found ? render_404 : render_403
else
require_login
end
end
# Find project of id params[:id]
# Note: find() is Project.friendly.find()
def find_project
2026-02-02 13:36:51 +01:00
@project = Project.visible.find(params[:id])
end
2016-09-08 10:58:48 +02:00
# Find project of id params[:project_id]
# Note: find() is Project.friendly.find()
def find_project_by_project_id
2026-02-02 13:36:51 +01:00
@project = Project.visible.find(params[:project_id])
2016-09-08 10:58:48 +02:00
end
2025-09-23 08:32:19 +02:00
# Find project by project_id if given
def find_optional_project
2026-02-02 13:36:51 +01:00
@project = Project.visible.find(params[:project_id]) if params[:project_id].present?
rescue ActiveRecord::RecordNotFound
render_404
2025-09-23 08:32:19 +02:00
end
# Finds and sets @project based on @object.project
def find_project_from_association
render_404 if @object.blank?
2011-05-30 20:52:25 +02:00
@project = @object.project
end
# Filter for bulk work package operations. Either :work_package_id (single-WP
# routes) or :ids (bulk routes) may carry numeric or semantic identifiers
# ("PROJ-42") since both originate from human-facing URLs or forms.
def find_work_packages
@work_packages = WorkPackage.where_display_id_in(params[:work_package_id] || params[:ids])
.includes(:project)
.order("id ASC")
fail ActiveRecord::RecordNotFound if @work_packages.empty?
2021-02-11 16:02:18 +01:00
2023-09-06 11:06:36 +02:00
@projects = @work_packages.filter_map(&:project).uniq
@project = @projects.first if @projects.size == 1
end
2011-05-30 20:52:25 +02:00
def back_url
params[:back_url] || request.env["HTTP_REFERER"]
end
2025-02-13 11:01:15 +01:00
def redirect_back_or_default(default, use_escaped: true, status: :found)
2015-09-13 19:56:12 +02:00
policy = RedirectPolicy.new(
params[:back_url],
hostname: request.host,
default:,
2018-02-13 07:52:58 +01:00
return_escaped: use_escaped
2015-09-13 19:56:12 +02:00
)
2025-02-13 11:01:15 +01:00
redirect_to(policy.redirect_url, status:)
2015-04-07 14:10:58 +02:00
end
# Picks which layout to use based on the request
#
# @return [boolean, string] name of the layout to use or false for no layout
def use_layout
request.xhr? ? false : "no_menu"
end
2011-05-30 20:52:25 +02:00
def render_feed(items, options = {})
@items = items || []
2016-04-22 10:39:26 +02:00
@items = @items.sort { |x, y| y.event_datetime <=> x.event_datetime }
@items = @items.slice(0, Setting.feeds_limit.to_i)
2007-08-29 16:52:35 +00:00
@title = options[:title] || Setting.app_title
2014-11-03 21:10:20 +01:00
render template: "common/feed", layout: false, content_type: "application/atom+xml"
2007-08-29 16:52:35 +00:00
end
2011-05-30 20:52:25 +02:00
2007-08-29 16:52:35 +00:00
def self.accept_key_auth(*actions)
actions = actions.flatten.map(&:to_s)
2012-08-31 13:56:33 +02:00
self.accept_key_auth_actions = actions
2007-08-29 16:52:35 +00:00
end
2011-05-30 20:52:25 +02:00
2007-08-29 16:52:35 +00:00
def accept_key_auth_actions
2012-08-31 13:56:33 +02:00
self.class.accept_key_auth_actions || []
2007-08-29 16:52:35 +00:00
end
2011-05-30 20:52:25 +02:00
2008-01-10 22:42:41 +00:00
# Returns a string that can be used as filename value in Content-Disposition header
def filename_for_content_disposition(name)
2023-09-06 12:50:32 +02:00
%r{(MSIE|Trident)}.match?(request.env["HTTP_USER_AGENT"]) ? ERB::Util.url_encode(name) : name
2008-01-10 22:42:41 +00:00
end
2011-05-30 20:52:25 +02:00
def api_request?
2013-04-24 14:16:52 +02:00
if params[:format].nil?
%w(application/xml application/json).include? request.format.to_s
else
%w(xml json).include? params[:format]
end
end
2011-05-30 20:52:25 +02:00
# Returns the API key present in the request
def api_key_from_request
if params[:key].present?
params[:key]
elsif request.headers["X-OpenProject-API-Key"].present?
request.headers["X-OpenProject-API-Key"]
end
end
2010-06-05 03:52:59 +00:00
# Converts the errors on an ActiveRecord object into a common JSON format
def object_errors_to_json(object)
2019-04-08 08:11:39 +02:00
object.errors.map do |attribute, error|
2010-06-05 03:52:59 +00:00
{ attribute => error }
2019-04-08 08:11:39 +02:00
end.to_json
2010-06-05 03:52:59 +00:00
end
# Renders API response on validation failure
def render_validation_errors(object)
2014-11-03 21:10:20 +01:00
options = { status: :unprocessable_entity, layout: false }
errors = case params[:format]
when "xml"
{ xml: object.errors }
when "json"
{ json: { "errors" => object.errors } } # ActiveResource client compliance
else
fail "Unknown format #{params[:format]} in #render_validation_errors"
end
options.merge! errors
render options
end
2011-05-30 20:52:25 +02:00
# Overrides #default_template so that the api template
# is used automatically if it exists
def default_template(action_name = self.action_name)
if api_request?
begin
return view_paths.find_template(default_template_name(action_name), "api")
rescue ::ActionView::MissingTemplate
# the api template was not found
# fallback to the default behaviour
end
end
super
end
2011-05-30 20:52:25 +02:00
# Overrides #pick_layout so that #render with no arguments
# doesn't use the layout for api requests
def pick_layout(*args)
api_request? ? nil : super
end
2012-02-14 14:05:25 +01:00
def admin_first_level_menu_entry
menu_item = admin_menu_item(current_menu_item)
menu_item.parent
end
helper_method :admin_first_level_menu_entry
def check_session_lifetime
2013-07-16 11:50:53 +02:00
if session_expired?
self.logged_user = nil
2014-11-03 21:10:20 +01:00
flash[:warning] = I18n.t("notice_forced_logout", ttl_time: Setting.session_ttl)
2019-01-09 09:03:56 +01:00
redirect_to(controller: "/account", action: "login", back_url: login_back_url)
end
session[:updated_at] = Time.now
end
2013-09-10 14:43:17 +02:00
def feed_request?
2013-09-10 10:03:55 +02:00
if params[:format].nil?
%w(application/rss+xml application/atom+xml).include? request.format.to_s
else
2013-09-10 14:43:17 +02:00
%w(atom rss).include? params[:format]
2013-09-10 10:03:55 +02:00
end
end
2013-09-10 14:43:17 +02:00
def stop_if_feeds_disabled
if feed_request? && !Setting.feeds_enabled?
render_404(message: I18n.t("label_disabled"))
2013-09-10 10:03:55 +02:00
end
end
private
def session_expired?
2017-06-29 08:30:40 +02:00
!api_request? && current_user.logged? && session_ttl_expired?
end
def permitted_params
@permitted_params ||= PermittedParams.new(params, current_user)
end
def login_back_url_params
{}
end
def login_back_url
# Extract only the basic url parameters on non-GET requests
if request.get?
# rely on url_for to fill in the parameters of the current request
url_for(login_back_url_params)
else
url_params = params.permit(:action, :id, :project_id, :controller)
unless url_params[:controller].to_s.starts_with?("/")
url_params[:controller] = "/#{url_params[:controller]}"
end
url_for(url_params)
end
end
2018-03-21 08:59:59 +01:00
def action_hooks
call_hook(:application_controller_before_action)
end
# ActiveSupport load hooks provide plugins with a consistent entry point to patch core classes.
# They should be called at the very end of a class definition or file,
# so plugins can be sure everything has been loaded. This load hook allows plugins to register
# callbacks when the core application controller is fully loaded. Good explanation of load hooks:
# http://simonecarletti.com/blog/2011/04/understanding-ruby-and-rails-lazy-load-hooks/
ActiveSupport.run_load_hooks(:application_controller, self)
2017-08-30 15:17:02 +01:00
2020-02-11 08:04:46 +01:00
prepend AuthSourceSSO
2007-08-29 16:52:35 +00:00
end