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 Carto::Common::ControllerHelper include ::LoggerControllerHelper protect_from_forgery helper :all around_filter :wrap_in_profiler around_action :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 current_user.try(:invalidate_all_sessions!) 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? 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 if user.try(:pending_verification?) render_unverified_user return end elsif current_user.try(:pending_verification?) render_unverified_user return 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? || (format.nil? && request.accepts.first.json?) 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