511 lines
16 KiB
Ruby
511 lines
16 KiB
Ruby
require_relative '../../lib/cartodb/profiler.rb'
|
|
require_dependency 'carto/authentication_manager'
|
|
require_dependency 'carto/http_header_authentication'
|
|
|
|
class ApplicationController < ActionController::Base
|
|
include UrlHelper
|
|
include Carto::ControllerHelper
|
|
include ::LoggerControllerHelper
|
|
|
|
protect_from_forgery
|
|
|
|
helper :all
|
|
|
|
around_filter :wrap_in_profiler
|
|
around_filter :set_request_id
|
|
|
|
before_filter :set_security_headers
|
|
before_filter :http_header_authentication, if: :http_header_authentication?
|
|
before_filter :store_request_host
|
|
before_filter :ensure_user_organization_valid
|
|
before_filter :ensure_org_url_if_org_user
|
|
before_filter :ensure_account_has_been_activated
|
|
before_filter :browser_is_html5_compliant?
|
|
before_filter :set_asset_debugging
|
|
before_filter :cors_preflight_check
|
|
before_filter :check_maintenance_mode
|
|
before_filter :check_user_state
|
|
after_filter :allow_cross_domain_access
|
|
after_filter :remove_flash_cookie
|
|
after_filter :add_revision_header
|
|
|
|
rescue_from NoHTML5Compliant, :with => :no_html5_compliant
|
|
rescue_from ActiveRecord::RecordNotFound, RecordNotFound, with: :render_404
|
|
rescue_from Carto::ExpiredSessionError, with: :rescue_from_carto_error
|
|
|
|
ME_ENDPOINT_COOKIE = :_cartodb_base_url
|
|
IGNORE_PATHS_FOR_CHECK_USER_STATE = %w(unverified maintenance_mode lockout login logout unauthenticated multifactor_authentication).freeze
|
|
|
|
def self.ssl_required(*splat)
|
|
if Cartodb.config[:ssl_required] == true
|
|
if splat.any?
|
|
force_ssl only: splat
|
|
else
|
|
force_ssl
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.ssl_allowed(*_splat)
|
|
# noop
|
|
end
|
|
|
|
# current_user relies on request subdomain ALWAYS, so current_viewer will always return:
|
|
# - If subdomain is present in the sessions: subdomain-based session (aka current_user)
|
|
# - Else: the first session found at request.session that comes from warden
|
|
def current_viewer
|
|
if @current_viewer.nil?
|
|
if current_user && env["warden"].authenticated?(current_user.username)
|
|
@current_viewer = current_user if Carto::AuthenticationManager.validate_session(warden, request, current_user)
|
|
else
|
|
authenticated_usernames = request.session.to_hash.select { |k, _|
|
|
k.start_with?("warden.user") && !k.end_with?(".session")
|
|
}.values
|
|
# See if there's a session of the viewed subdomain corresponding user
|
|
current_user_present = authenticated_usernames.select { |username|
|
|
CartoDB.extract_subdomain(request) == username
|
|
}.first
|
|
|
|
# If current user session was there, do nothing; else, retrieve first available
|
|
if current_user_present.nil?
|
|
unless authenticated_usernames.first.nil?
|
|
user = Carto::User.find_by(username: authenticated_usernames.first)
|
|
Carto::AuthenticationManager.validate_session(warden, request, user) unless user.nil?
|
|
@current_viewer = user
|
|
end
|
|
end
|
|
end
|
|
end
|
|
@current_viewer
|
|
rescue Carto::ExpiredSessionError => e
|
|
request.reset_session
|
|
not_authorized(e)
|
|
end
|
|
|
|
protected
|
|
|
|
Warden::Manager.after_authentication do |user, auth, opts|
|
|
auth.cookies.permanent[ME_ENDPOINT_COOKIE] = {
|
|
value: CartoDB.base_url(user.username),
|
|
domain: Cartodb.config[:session_domain]
|
|
} if opts[:store]
|
|
|
|
# Do not even send the Set-Cookie header if the strategy did not store anything in the session
|
|
auth.request.session_options[:skip] = true if opts[:store] == false
|
|
end
|
|
|
|
Warden::Manager.before_logout do |user, auth, opts|
|
|
if user.present?
|
|
user.invalidate_all_sessions!
|
|
elsif opts[:scope]
|
|
scope_user = Carto::User.find_by(username: opts[:scope])
|
|
scope_user&.invalidate_all_sessions!
|
|
end
|
|
auth.cookies.delete(ME_ENDPOINT_COOKIE, domain: Cartodb.config[:session_domain])
|
|
end
|
|
|
|
def handle_unverified_request
|
|
render_403
|
|
end
|
|
|
|
# @see Warden::Manager.after_set_user
|
|
def update_session_security_token(user)
|
|
warden.session(user.username)[:sec_token] = user.security_token
|
|
end
|
|
|
|
def is_https?
|
|
request.protocol == 'https://'
|
|
end
|
|
|
|
def http_header_authentication
|
|
authenticate(:http_header_authentication, scope: CartoDB.extract_subdomain(request))
|
|
if current_user
|
|
Carto::AuthenticationManager.validate_session(warden, request, current_user)
|
|
else
|
|
authenticator = Carto::HttpHeaderAuthentication.new
|
|
if authenticator.autocreation_enabled?
|
|
if authenticator.creation_in_progress?(request)
|
|
redirect_to CartoDB.path(self, 'signup_http_authentication_in_progress')
|
|
else
|
|
redirect_to CartoDB.path(self, 'signup_http_authentication')
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# To be used only when domainless urls are present, to replicate sent subdomain
|
|
def store_request_host
|
|
return unless CartoDB.subdomainless_urls?
|
|
|
|
match = /([\w\-\.]+)(:[\d]+)?\/?/.match(request.host.to_s)
|
|
unless match.nil?
|
|
CartoDB.request_host = match[1]
|
|
end
|
|
end
|
|
|
|
def wrap_in_profiler
|
|
if params[:profile_request].present? && current_user.present? && current_user.has_feature_flag?('profiler')
|
|
CartoDB::Profiler.new().call(request, response) { yield }
|
|
else
|
|
yield
|
|
end
|
|
end
|
|
|
|
def set_asset_debugging
|
|
CartoDB::Application.config.assets.debug =
|
|
(Cartodb.config[:debug_assets].nil? ? true : Cartodb.config[:debug_assets]) if Rails.env.development?
|
|
end
|
|
|
|
def cors_preflight_check
|
|
if request.method == :options && check_cors_headers_for_whitelisted_origin
|
|
common_cors_headers
|
|
response.headers['Access-Control-Max-Age'] = '3600'
|
|
elsif !Rails.env.production? && !Rails.env.staging?
|
|
development_cors_headers
|
|
end
|
|
end
|
|
|
|
def allow_cross_domain_access
|
|
if !request.headers['origin'].blank? && check_cors_headers_for_whitelisted_origin
|
|
common_cors_headers
|
|
response.headers['Access-Control-Allow-Credentials'] = 'true'
|
|
elsif !Rails.env.production? && !Rails.env.staging?
|
|
development_cors_headers
|
|
end
|
|
end
|
|
|
|
def common_cors_headers
|
|
response.headers['Access-Control-Allow-Origin'] = request.headers['origin']
|
|
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, DELETE'
|
|
response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
|
|
end
|
|
|
|
def development_cors_headers
|
|
response.headers['Access-Control-Allow-Origin'] = '*'
|
|
response.headers['Access-Control-Allow-Methods'] = '*'
|
|
response.headers['Access-Control-Allow-Headers'] = '*'
|
|
end
|
|
|
|
def check_cors_headers_for_whitelisted_origin
|
|
origin = request.headers['origin']
|
|
|
|
cors_enabled_hosts = Cartodb.get_config(:cors_enabled_hosts) || []
|
|
allowed_hosts = ([Cartodb.config[:account_host]] + cors_enabled_hosts).compact
|
|
|
|
allowed_hosts.include?(URI.parse(origin).host)
|
|
end
|
|
|
|
def check_user_state
|
|
return if IGNORE_PATHS_FOR_CHECK_USER_STATE.any? { |path| request.path.end_with?("/" + path) }
|
|
|
|
viewed_username = CartoDB.extract_subdomain(request)
|
|
if current_user.nil? || current_user.username != viewed_username
|
|
user = Carto::User.find_by_username(viewed_username)
|
|
if user.try(:locked?)
|
|
render_locked_owner
|
|
return
|
|
end
|
|
elsif current_user.locked?
|
|
render_locked_user
|
|
return
|
|
elsif current_user.unverified?
|
|
render_unverified_user
|
|
return
|
|
end
|
|
|
|
render_multifactor_authentication if multifactor_authentication_required?
|
|
end
|
|
|
|
def check_maintenance_mode
|
|
return if IGNORE_PATHS_FOR_CHECK_USER_STATE.any? { |path| request.path.end_with?("/" + path) }
|
|
|
|
viewed_username = CartoDB.extract_subdomain(request)
|
|
if current_user.nil? || current_user.username != viewed_username
|
|
user = Carto::User.find_by_username(viewed_username)
|
|
if user.try(:maintenance_mode?)
|
|
render_locked_owner
|
|
return
|
|
end
|
|
elsif current_user.maintenance_mode?
|
|
render_maintenance_mode
|
|
return
|
|
end
|
|
end
|
|
|
|
def render_403
|
|
respond_to do |format|
|
|
format.html { render(file: 'public/403.html', status: 403, layout: false) }
|
|
format.all { head(:forbidden) }
|
|
end
|
|
end
|
|
|
|
def render_404
|
|
respond_to do |format|
|
|
format.html do
|
|
render :file => 'public/404.html', :status => 404, :layout => false
|
|
end
|
|
format.json do
|
|
head :not_found
|
|
end
|
|
end
|
|
end
|
|
|
|
def render_500
|
|
render_http_code(500)
|
|
end
|
|
|
|
def render_http_code(error_code, public_page_error_code = error_code, error_message = 'Unknown error')
|
|
respond_to do |format|
|
|
format.html do
|
|
render file: "public/#{public_page_error_code}.html", status: error_code, layout: false
|
|
end
|
|
format.json do
|
|
render json: { error_message: error_message }, status: error_code
|
|
end
|
|
end
|
|
end
|
|
|
|
def multifactor_authentication_required?(user = current_viewer)
|
|
user&.multifactor_authentication_configured? &&
|
|
!warden.session(user.username)[:multifactor_authentication_performed] &&
|
|
!warden.session(user.username)[:skip_multifactor_authentication]
|
|
rescue Warden::NotAuthenticated
|
|
false
|
|
end
|
|
|
|
def login_required
|
|
is_auth = authenticated?(CartoDB.extract_subdomain(request))
|
|
is_auth ? Carto::AuthenticationManager.validate_session(warden, request, current_user) : not_authorized
|
|
end
|
|
|
|
def login_required_any_user
|
|
current_viewer ? Carto::AuthenticationManager.validate_session(warden, request, current_viewer) : not_authorized
|
|
end
|
|
|
|
def api_authorization_required
|
|
authenticate!(:auth_api, :api_authentication, scope: CartoDB.extract_subdomain(request))
|
|
Carto::AuthenticationManager.validate_session(warden, request, current_user)
|
|
end
|
|
|
|
def any_api_authorization_required
|
|
authenticate!(:any_auth_api, :api_authentication, scope: CartoDB.extract_subdomain(request))
|
|
Carto::AuthenticationManager.validate_session(warden, request, current_user)
|
|
end
|
|
|
|
def engine_required
|
|
render_404 unless current_viewer.try(:engine_enabled?)
|
|
end
|
|
|
|
# This only allows to authenticate if sending an API request to username.api_key subdomain,
|
|
# but doesn't break the request if can't authenticate
|
|
def optional_api_authorization
|
|
got_auth = authenticate(:auth_api, :api_authentication, scope: CartoDB.extract_subdomain(request))
|
|
Carto::AuthenticationManager.validate_session(warden, request, current_user) if got_auth
|
|
rescue Carto::ExpiredSessionError => e
|
|
not_authorized(e)
|
|
end
|
|
|
|
def redirect_or_forbidden(path, error)
|
|
respond_to do |format|
|
|
format.html do
|
|
redirect_to CartoDB.url(self, path)
|
|
end
|
|
format.json do
|
|
render(json: { error: error }, status: 403)
|
|
end
|
|
end
|
|
end
|
|
|
|
def render_multifactor_authentication
|
|
session[:return_to] = request.original_url
|
|
redirect_or_forbidden('multifactor_authentication_session', 'mfa_required')
|
|
end
|
|
|
|
def render_unverified_user
|
|
redirect_or_forbidden('unverified', 'unverified')
|
|
end
|
|
|
|
def render_locked_user
|
|
redirect_or_forbidden('lockout', 'lockout')
|
|
end
|
|
|
|
def render_maintenance_mode
|
|
redirect_or_forbidden('maintenance_mode', 'maintenance_mode')
|
|
end
|
|
|
|
def render_locked_owner
|
|
respond_to do |format|
|
|
format.html do
|
|
render_404
|
|
end
|
|
format.json do
|
|
head 404
|
|
end
|
|
end
|
|
end
|
|
|
|
def not_authorized(exception = nil)
|
|
respond_to do |format|
|
|
format.html do
|
|
session[:return_to] = request.url
|
|
redirect_to CartoDB.url(self, 'login', keep_base_url: true)
|
|
return
|
|
end
|
|
format.json do
|
|
render(json: { errors: exception&.message }, status: :unauthorized)
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
def table_privacy_text(table)
|
|
if table.is_a?(::Table)
|
|
table.privacy_text
|
|
elsif table.is_a?(Hash)
|
|
table['privacy']
|
|
end
|
|
end
|
|
helper_method :table_privacy_text
|
|
|
|
# TODO: Move to own exception infrastructure
|
|
def translate_error(exception)
|
|
return exception if exception.blank? || exception.is_a?(String)
|
|
|
|
case exception
|
|
when CartoDB::EmptyFile
|
|
when CartoDB::InvalidUrl
|
|
when CartoDB::InvalidFile
|
|
when CartoDB::TableCopyError
|
|
when CartoDB::QuotaExceeded
|
|
exception.detail
|
|
when Sequel::DatabaseError
|
|
# TODO: rationalise these error codes
|
|
if exception.message.include?("transform: couldn't project")
|
|
Cartodb.error_codes[:geometries_error].merge(:raw_error => exception.message)
|
|
else
|
|
Cartodb.error_codes[:unknown_error].merge(:raw_error => exception.message)
|
|
end
|
|
else
|
|
Cartodb.error_codes[:unknown_error].merge(:raw_error => exception.message)
|
|
end.to_json
|
|
end
|
|
|
|
def no_html5_compliant
|
|
logout
|
|
render :file => "#{Rails.root}/public/HTML5.html", :status => 500, :layout => false
|
|
end
|
|
|
|
# In some cases the flash message is going to be set in the fronted with js after making a request to the API
|
|
# We use this filter to ensure it disappears in the very first request
|
|
def remove_flash_cookie
|
|
cookies.delete(:flash) if cookies[:flash]
|
|
end
|
|
|
|
def browser_is_html5_compliant?
|
|
user_agent = request.user_agent.try(:downcase)
|
|
|
|
return true if user_agent.nil?
|
|
|
|
banned_regex = [
|
|
/msie [0-9]\./, /safari\/[0-4][0-2][0-2]/, /opera\/[0-8].[0-7]/, /firefox\/[0-2].[0-5]/
|
|
]
|
|
|
|
if banned_regex.map { |re| user_agent.match(re) }.compact.first
|
|
raise NoHTML5Compliant
|
|
end
|
|
end
|
|
|
|
def ensure_user_organization_valid
|
|
return if CartoDB.subdomainless_urls?
|
|
|
|
org_subdomain = CartoDB.extract_host_subdomain(request)
|
|
unless org_subdomain.nil? || current_user.nil?
|
|
if current_user.organization.nil? || current_user.organization.name != org_subdomain
|
|
render_404
|
|
end
|
|
end
|
|
end
|
|
|
|
# By default, override Admin urls unless :dont_rewrite param is present
|
|
def ensure_org_url_if_org_user
|
|
return if CartoDB.subdomainless_urls?
|
|
|
|
rewrite_url = !request.params[:dont_rewrite].present?
|
|
if rewrite_url && !current_user.nil? && !current_user.organization.nil? &&
|
|
CartoDB.subdomain_from_request(request) == current_user.username
|
|
if request.fullpath == '/'
|
|
redirect_to CartoDB.url(self, 'dashboard')
|
|
else
|
|
redirect_to CartoDB.base_url(current_user.organization.name, current_user.username) << request.fullpath
|
|
end
|
|
end
|
|
end
|
|
|
|
def ensure_account_has_been_activated
|
|
return unless current_user
|
|
|
|
if !current_user.enable_account_token.nil?
|
|
respond_to do |format|
|
|
format.html {
|
|
redirect_to CartoDB.url(self, 'account_token_authentication_error')
|
|
}
|
|
format.all {
|
|
head :forbidden
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
def add_revision_header
|
|
response.headers['X-CartoDB-Rev'] = CartoDB::CARTODB_REV unless CartoDB::CARTODB_REV.nil?
|
|
end
|
|
|
|
def current_user
|
|
super(CartoDB.extract_subdomain(request))
|
|
end
|
|
|
|
def update_user_last_activity
|
|
return false if current_user.nil?
|
|
current_user.set_last_active_time
|
|
current_user.set_last_ip_address request.remote_ip
|
|
end
|
|
|
|
def ensure_required_params(required_params, status = 400)
|
|
params_with_value = params.reject { |_, v| v.empty? }
|
|
missing_params = required_params - params_with_value.keys
|
|
raise Carto::MissingParamsError.new(missing_params, status) unless missing_params.empty?
|
|
end
|
|
|
|
def ensure_required_request_params(required_params, status = 422)
|
|
params_with_value = request.request_parameters.reject { |_, v| v.empty? }
|
|
missing_params = required_params - params_with_value.keys
|
|
raise Carto::UnprocesableEntityError.new("Missing parameter: #{missing_params}", status) unless missing_params.empty?
|
|
end
|
|
|
|
def ensure_no_extra_request_params(allowed_params, status = 422)
|
|
params_with_value = request.request_parameters.reject { |_, v| v.empty? }
|
|
extra_params = params_with_value.keys - allowed_params
|
|
raise Carto::UnprocesableEntityError.new("Invalid parameter: #{extra_params}", status) unless extra_params.empty?
|
|
end
|
|
|
|
protected :current_user
|
|
|
|
def json_formatted_request?
|
|
format = request.format
|
|
|
|
format.json? if format
|
|
end
|
|
|
|
private
|
|
|
|
def http_header_authentication?
|
|
Carto::HttpHeaderAuthentication.new.valid?(request)
|
|
end
|
|
|
|
def set_security_headers
|
|
headers['X-Frame-Options'] = 'DENY'
|
|
headers['X-XSS-Protection'] = '1; mode=block'
|
|
headers['X-Content-Type-Options'] = 'nosniff'
|
|
end
|
|
end
|