module Carto module Api module Public class FederatedTablesController < Carto::Api::Public::ApplicationController include Carto::Api::PagedSearcher extend Carto::DefaultRescueFroms VALID_ORDER_PARAMS_FEDERATED_SERVER = %i(federated_server_name).freeze VALID_ORDER_PARAMS_REMOTE_SCHEMA = %i(remote_schema_name).freeze VALID_ORDER_PARAMS_REMOTE_TABLE = %i(remote_table_name).freeze FEDERATED_SERVER_ATTRIBUTES = %i(federated_server_name mode dbname host port username password).freeze REMOTE_TABLE_ATTRIBUTES = %i(federated_server_name remote_schema_name remote_table_name local_table_name_override id_column_name geom_column_name webmercator_column_name).freeze REQUIRED_POST_FEDERATED_SERVER_ATTRIBUTES = %w{ federated_server_name mode host username password }.freeze REQUIRED_PUT_FEDERATED_SERVER_ATTRIBUTES = %w{ mode host username password }.freeze ALLOWED_PUT_FEDERATED_SERVER_ATTRIBUTES = %w{ mode dbname host port username password }.freeze REQUIRED_POST_REMOTE_TABLE_ATTRIBUTES = %w{ remote_table_name id_column_name }.freeze REQUIRED_PUT_REMOTE_TABLE_ATTRIBUTES = %w{ id_column_name }.freeze ALLOWED_PUT_REMOTE_TABLE_ATTRIBUTES = %w{ local_table_name_override id_column_name geom_column_name webmercator_column_name }.freeze before_action :load_user before_action :check_federated_tables_enable before_action :check_permissions before_action :load_service # Federated Servers before_action only: [:list_federated_servers] do load_pagination_params(default_order: 'federated_server_name', valid_order_params: VALID_ORDER_PARAMS_FEDERATED_SERVER) end before_action :load_federated_server_attributes, only: [:register_federated_server, :update_federated_server ] before_action :load_federated_server, only: [:update_federated_server, :unregister_federated_server, :show_federated_server] before_action :check_federated_server, only: [:unregister_federated_server, :show_federated_server] before_action :ensure_required_federated_server_attributes, only: [:register_federated_server, :update_federated_server] before_action :validate_federated_server_attributes, only: [:register_federated_server, :update_federated_server] # Remote Schemas before_action only: [:list_remote_schemas] do load_pagination_params(default_order: 'remote_schema_name', valid_order_params: VALID_ORDER_PARAMS_REMOTE_SCHEMA) end # Remote Tables before_action only: [:list_remote_tables] do load_pagination_params(default_order: 'remote_table_name', valid_order_params: VALID_ORDER_PARAMS_REMOTE_TABLE) end before_action :load_remote_table_attributes, only: [:register_remote_table, :update_remote_table ] before_action :load_remote_table, only: [:update_remote_table, :unregister_remote_table, :show_remote_table] before_action :check_remote_table, only: [:unregister_remote_table, :show_remote_table] before_action :ensure_required_remote_table_attributes, only: [:register_remote_table, :update_remote_table] before_action :ensure_readonly_mode, only: [:register_federated_server, :update_federated_server] setup_default_rescues rescue_from Sequel::DatabaseError, with: :rescue_from_service_error # Federated Servers def list_federated_servers result = @service.list_servers(@pagination) total = @service.count_servers render_paged(result, total) end def register_federated_server federated_server = @service.register_server(@federated_server_attributes) response.headers['Content-Location'] = "#{request.path}/#{federated_server[:federated_server_name]}" render_jsonp(federated_server, 201) end def show_federated_server render_jsonp(@federated_server, 200) end def update_federated_server unless @federated_server @federated_server = @service.register_server(@federated_server_attributes) response.headers['Content-Location'] = "#{request.path}" return render_jsonp(@federated_server, 201) end @federated_server = @service.update_server(@federated_server_attributes) render_jsonp({}, 204) end def unregister_federated_server @service.unregister_server(federated_server_name: params[:federated_server_name]) render_jsonp({}, 204) end # Remote Schemas def list_remote_schemas result = @service.list_remote_schemas(federated_server_name: params[:federated_server_name], **@pagination) total = @service.count_remote_schemas(federated_server_name: params[:federated_server_name]) render_paged(result, total) end # Remote Tables def list_remote_tables result = @service.list_remote_tables( federated_server_name: params[:federated_server_name], remote_schema_name: params[:remote_schema_name], **@pagination ) # For unregistered tables we only want to keep the relevant properties result.each {|table| table.slice!(:registered, :remote_schema_name, :remote_table_name, :columns) unless table[:registered]} total = @service.count_remote_tables( federated_server_name: params[:federated_server_name], remote_schema_name: params[:remote_schema_name] ) render_paged(result, total) end def register_remote_table remote_table = @service.register_table(@remote_table_attributes) response.headers['Content-Location'] = "#{request.path}/#{remote_table[:remote_table_name]}" render_jsonp(remote_table, 201) end def show_remote_table render_jsonp(@remote_table, 200) end def update_remote_table unless @remote_table[:registered] @remote_table = @service.register_table(@remote_table_attributes) response.headers['Content-Location'] = "#{request.path}" return render_jsonp(@remote_table, 201) end @remote_table = @service.update_table(@remote_table_attributes) render_jsonp({}, 204) end def unregister_remote_table @service.unregister_table( federated_server_name: params[:federated_server_name], remote_schema_name: params[:remote_schema_name], remote_table_name: params[:remote_table_name] ) render_jsonp({}, 204) end private def load_user @user = ::User.where(id: current_viewer.id).first end def load_service @service = Carto::FederatedTablesService.new(user: @user) end def load_pagination_params(default_order:, valid_order_params:) page, per_page, order, direction = page_per_page_order_params( valid_order_params, default_order: default_order, default_order_direction: 'asc' ) offset = (page - 1) * per_page @pagination = { page: page, per_page: per_page, order: order, direction: direction, offset: offset } end def load_federated_server_attributes @federated_server_attributes = params.slice(*FEDERATED_SERVER_ATTRIBUTES).permit(*FEDERATED_SERVER_ATTRIBUTES).to_h.symbolize_keys end def load_federated_server @federated_server = @service.get_server(federated_server_name: params[:federated_server_name]) end def validate_federated_server_attributes name = @federated_server_attributes[:federated_server_name] raise Carto::InvalidParameterFormatError.new('federated_server_name', "The value #{name} must be lowercase") unless name.strip.downcase == name end def check_federated_server raise Carto::LoadError.new("Federated server not found: #{params[:federated_server_name]}") unless @federated_server end def ensure_required_federated_server_attributes if request.post? ensure_required_request_params(REQUIRED_POST_FEDERATED_SERVER_ATTRIBUTES) else ensure_required_request_params(REQUIRED_PUT_FEDERATED_SERVER_ATTRIBUTES) ensure_no_extra_request_params(ALLOWED_PUT_FEDERATED_SERVER_ATTRIBUTES) end end def load_remote_table_attributes @remote_table_attributes = params.slice(*REMOTE_TABLE_ATTRIBUTES).permit(*REMOTE_TABLE_ATTRIBUTES).to_h.symbolize_keys @remote_table_attributes[:local_table_name_override] ||= @remote_table_attributes[:remote_table_name] end def load_remote_table @remote_table = @service.get_remote_table( federated_server_name: params[:federated_server_name], remote_schema_name: params[:remote_schema_name], remote_table_name: params[:remote_table_name] ) raise Carto::LoadError.new("Table '#{params[:remote_schema_name]}'.'#{params[:remote_table_name]}' not found at '#{params[:federated_server_name]}'") if @remote_table.nil? end def check_remote_table raise Carto::LoadError.new("Remote table key not found: #{params[:federated_server_name]}/#{params[:remote_schema_name]}.#{params[:remote_table_name]}") unless @remote_table end def ensure_required_remote_table_attributes if request.post? ensure_required_request_params(REQUIRED_POST_REMOTE_TABLE_ATTRIBUTES) else ensure_required_request_params(REQUIRED_PUT_REMOTE_TABLE_ATTRIBUTES) ensure_no_extra_request_params(ALLOWED_PUT_REMOTE_TABLE_ATTRIBUTES, 422) end end def ensure_readonly_mode raise Carto::UnprocesableEntityError.new("Invalid access mode: '#{params[:mode]}'. Only 'read-only' accepted") unless ("read-only".casecmp params[:mode]) == 0 end def check_permissions @api_key = Carto::ApiKey.find_by_token(params["api_key"]) raise UnauthorizedError unless @api_key&.master? raise UnauthorizedError unless @api_key.user_id === @user.id end def check_federated_tables_enable raise UnauthorizedError.new('Federated Tables not enabled') unless @user.has_feature_flag?('federated_tables') end def render_paged(result, total) enriched_response = paged_result( result: result, total_count: total, page: @pagination[:page], per_page: @pagination[:per_page], params: params.except('controller', 'action') ) { |params| api_v4_federated_servers_list_servers_url(params) } render_jsonp(enriched_response, 200) end def rescue_from_service_error(exception) log_rescue_from(__method__, exception) message = get_error_message(exception) case message when /(.*) does not exist/ rescue_from_carto_error(Carto::LoadError.new(message)) when /Not enough permissions to access the server (.*)/ rescue_from_carto_error(Carto::UnauthorizedError.new(message)) when /Server name (.*) is too long to be used as identifier/, /Could not import table (.*) of server (.*)/, /Could not import table (.*) as (.*) already exists/, /non integer id_column (.*)/, /non geometry column (.*)/ rescue_from_carto_error(Carto::UnprocesableEntityError.new(message)) else raise exception end end def get_error_message(exception) regex = /^PG::(.*): ERROR: / message = exception.message.split("\n").find { |s| s.match(regex) }.to_s.gsub(regex, '') raise exception.message unless message.present? message end end end end end