From 9b71178d2393aaed3e95a9adf5d6222a5133bb27 Mon Sep 17 00:00:00 2001 From: Achilleas Buisman Date: Sat, 6 Aug 2022 22:22:02 +0200 Subject: [PATCH 01/10] Add sharing feature to queries --- app/controllers/blazer/queries_controller.rb | 11 +++++- .../blazer/queries/_sharing_modal.html.erb | 29 +++++++++++++++ app/views/blazer/queries/show.html.erb | 12 +++++-- config/routes.rb | 1 + lib/blazer.rb | 8 +++++ lib/blazer/sharing.rb | 36 +++++++++++++++++++ test/queries_test.rb | 9 +++++ 7 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 app/views/blazer/queries/_sharing_modal.html.erb create mode 100644 lib/blazer/sharing.rb diff --git a/app/controllers/blazer/queries_controller.rb b/app/controllers/blazer/queries_controller.rb index 0a16795d3..9536fbe1c 100644 --- a/app/controllers/blazer/queries_controller.rb +++ b/app/controllers/blazer/queries_controller.rb @@ -78,6 +78,14 @@ def show def edit end + def share + if params[:token] && params[:query_id] && params[:token] == Blazer.sharing.query_token(params[:query_id]) + run + else + render_forbidden + end + end + def run @query = Query.find_by(id: params[:query_id]) if params[:query_id] @@ -87,7 +95,8 @@ def run data_source ||= params[:data_source] @data_source = Blazer.data_sources[data_source] - @statement = Blazer::Statement.new(params[:statement], @data_source) + sql_statement = params[:statement] || @query.statement + @statement = Blazer::Statement.new(sql_statement, @data_source) # before process_vars @cohort_analysis = @statement.cohort_analysis? diff --git a/app/views/blazer/queries/_sharing_modal.html.erb b/app/views/blazer/queries/_sharing_modal.html.erb new file mode 100644 index 000000000..721ceea49 --- /dev/null +++ b/app/views/blazer/queries/_sharing_modal.html.erb @@ -0,0 +1,29 @@ + diff --git a/app/views/blazer/queries/show.html.erb b/app/views/blazer/queries/show.html.erb index 54d8399a5..301206c84 100644 --- a/app/views/blazer/queries/show.html.erb +++ b/app/views/blazer/queries/show.html.erb @@ -10,17 +10,21 @@
-
+
<%= render partial: "blazer/nav" %>

<%= @query.name %>

-
+
<%= link_to "Edit", edit_query_path(@query, params: variable_params(@query)), class: "btn btn-default", disabled: !@query.editable?(blazer_user) %> <%= link_to "Fork", new_query_path(params: variable_params(@query).merge(fork_query_id: @query.id, data_source: @query.data_source, name: @query.name)), class: "btn btn-info" %> <% if !@error && @success %> + <% if Blazer.sharing.enabled? %> + Share + <% end %> + <%= button_to "Download", run_queries_path(format: "csv"), params: run_data, class: "btn btn-primary" %> <% end %>
@@ -78,3 +82,7 @@ } <% end %> + +<% if Blazer.sharing.enabled? %> + <%= render(partial: 'sharing_modal') %> +<% end %> diff --git a/config/routes.rb b/config/routes.rb index 5df190d91..18afa62ac 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -6,6 +6,7 @@ get :tables, on: :collection get :schema, on: :collection get :docs, on: :collection + get Blazer.sharing.route_path, to: 'queries#share', as: :share end resources :checks, except: [:show] do diff --git a/lib/blazer.rb b/lib/blazer.rb index 0fe038947..ec08cc0b0 100644 --- a/lib/blazer.rb +++ b/lib/blazer.rb @@ -12,6 +12,7 @@ require "blazer/data_source" require "blazer/result" require "blazer/run_statement" +require "blazer/sharing" require "blazer/statement" # adapters @@ -135,6 +136,13 @@ def self.data_sources end end + def self.sharing + @sharing ||= begin + sharing_settings = settings["sharing"] || {} + Blazer::Sharing.new(**sharing_settings.symbolize_keys) + end + end + # TODO move to Statement and remove in 3.0.0 def self.extract_vars(statement) # strip commented out lines diff --git a/lib/blazer/sharing.rb b/lib/blazer/sharing.rb new file mode 100644 index 000000000..3f9da9e8f --- /dev/null +++ b/lib/blazer/sharing.rb @@ -0,0 +1,36 @@ +module Blazer + class Sharing + attr_accessor :api_key, :path + + def initialize(api_key: ENV.fetch('BLAZER_DOWNLOAD_API_KEY', nil), path: '/blazer_share') + @api_key = api_key + @path = path.sub(/\/$/, '') # Strip trailing / + end + + def route_path + @route_path ||= "#{path}/:token/:query_id" + end + + def to_controller + 'blazer/queries#share' + end + + def query_token(query_id) + Digest::SHA1.hexdigest("#{query_id}-#{ENV.fetch('BLAZER_DOWNLOAD_API_KEY')}") + end + + def enabled? + api_key.present? + end + + def share_path(query_id, format: nil) + "#{path}/#{query_token(query_id)}/#{query_id}#{".#{format}" if format}" + end + + def url_for(query_id, current_url, format: 'csv') + url = URI.parse(current_url) + url.path = share_path(query_id, format: format) + url.to_s + end + end +end diff --git a/test/queries_test.rb b/test/queries_test.rb index 1c51d9e75..5d508f4e7 100644 --- a/test/queries_test.rb +++ b/test/queries_test.rb @@ -107,6 +107,15 @@ def test_csv assert_equal "id,city\n1,Chicago\n", response.body end + def test_share + ENV['BLAZER_DOWNLOAD_API_KEY'] = '123' + query = create_query + get blazer.query_share_path(query_id: query.id, token: Digest::SHA1.hexdigest("#{query.id}-123"), format: 'csv') + assert_response :success + assert_match query.name, response.body + ENV.delete('BLAZER_DOWNLOAD_API_KEY') + end + def test_url run_query "SELECT 'http://localhost:3000/'" assert_match %{http://localhost:3000/}, response.body From 49eabe183dec04d3255fdbd70eff745a7993a375 Mon Sep 17 00:00:00 2001 From: Achilleas Buisman Date: Sun, 7 Aug 2022 08:58:18 +0200 Subject: [PATCH 02/10] Make tests of sharing independent of ENV --- test/queries_test.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/queries_test.rb b/test/queries_test.rb index 5d508f4e7..bd78a219f 100644 --- a/test/queries_test.rb +++ b/test/queries_test.rb @@ -108,12 +108,12 @@ def test_csv end def test_share - ENV['BLAZER_DOWNLOAD_API_KEY'] = '123' + Blazer.sharing.api_key = "123" query = create_query get blazer.query_share_path(query_id: query.id, token: Digest::SHA1.hexdigest("#{query.id}-123"), format: 'csv') assert_response :success assert_match query.name, response.body - ENV.delete('BLAZER_DOWNLOAD_API_KEY') + Blazer.sharing.api_key = nil end def test_url From cf79e566402ff923a729b1bd655868f5482a43e3 Mon Sep 17 00:00:00 2001 From: Achilleas Buisman Date: Sun, 7 Aug 2022 09:09:54 +0200 Subject: [PATCH 03/10] Add README for sharing --- README.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/README.md b/README.md index aaa02a767..d80ed1bcc 100644 --- a/README.md +++ b/README.md @@ -297,6 +297,47 @@ cache: Of course, you can force a refresh at any time. +### Sharing (optional) + +You can generate sharing urls for queries. You can download up-to-date results for each query in CSV directly. + +This is useful for scripts or for automatic importing into spreadsheets. + +There are 2 steps necessary for setting up sharing: + +1. Configuring an API key +2. Make the sharing endpoint accessible in your routes + +First configure an API key in `blazer.yml`: + +```yml +sharing: + api_key: 'secret' +``` + +Alternatively you can set the `BLAZER_DOWNLOAD_API_KEY` ENV var which blazer uses by default. + +Now routes: we assume you have secured blazer so you will need to expose a new route outside of the mount. + +The default path for shares is `/blazer_share`. You can change this in `blazer.yml`: + +```yml +sharing: + path: /another_path +``` + +This config is only so that blazer can generate the correct url. + +Now add this route to your `routes.rb`: + +```ruby + get Blazer.sharing.route_path, to: Blazer.sharing.to_controller if Blazer.sharing.enabled? +``` + +Now restart your server and each query page will have a `share` button which will open up a modal that allows you to copy sharing urls. + +Each url has a unique token based on a hash of the query's id and the API key, so the token can't be reused for other queries. + ## Charts Blazer will automatically generate charts based on the types of the columns returned in your query. From 5a381815c2d33aeafaf225a12748ee58f8ef1e01 Mon Sep 17 00:00:00 2001 From: Achilleas Buisman Date: Sun, 7 Aug 2022 09:13:55 +0200 Subject: [PATCH 04/10] Correct id on google sheet sharing input --- app/views/blazer/queries/_sharing_modal.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/blazer/queries/_sharing_modal.html.erb b/app/views/blazer/queries/_sharing_modal.html.erb index 721ceea49..05400d086 100644 --- a/app/views/blazer/queries/_sharing_modal.html.erb +++ b/app/views/blazer/queries/_sharing_modal.html.erb @@ -17,7 +17,7 @@
- ")'> + ")'>
-
From cda2ffd40d61aa53a05d1a8be9efbbf3748452ce Mon Sep 17 00:00:00 2001 From: Achilleas Buisman Date: Wed, 5 Oct 2022 13:56:20 +0200 Subject: [PATCH 06/10] Use api_key --- lib/blazer/sharing.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/blazer/sharing.rb b/lib/blazer/sharing.rb index 3f9da9e8f..b514a21a9 100644 --- a/lib/blazer/sharing.rb +++ b/lib/blazer/sharing.rb @@ -16,7 +16,7 @@ def to_controller end def query_token(query_id) - Digest::SHA1.hexdigest("#{query_id}-#{ENV.fetch('BLAZER_DOWNLOAD_API_KEY')}") + Digest::SHA1.hexdigest("#{query_id}-#{api_key}") end def enabled? From 1e2628aafe89c928ee17f231d2b0ecd25fd2f1ea Mon Sep 17 00:00:00 2001 From: Achilleas Buisman Date: Fri, 7 Oct 2022 16:23:33 +0200 Subject: [PATCH 07/10] Fix dumb mistake of not creating the correct urls --- app/views/blazer/queries/_sharing_modal.html.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/blazer/queries/_sharing_modal.html.erb b/app/views/blazer/queries/_sharing_modal.html.erb index b5957ac4b..f47641a64 100644 --- a/app/views/blazer/queries/_sharing_modal.html.erb +++ b/app/views/blazer/queries/_sharing_modal.html.erb @@ -12,12 +12,12 @@
- +
- ")'> + ")'>
From bab1890623113c59c87f63f8c344e5effd3ecbcf Mon Sep 17 00:00:00 2001 From: Achilleas Buisman Date: Mon, 11 Sep 2023 23:07:29 +0200 Subject: [PATCH 08/10] Replace api key with token per query --- app/controllers/blazer/queries_controller.rb | 11 +++++---- app/models/blazer/query.rb | 6 +++++ lib/blazer/sharing.rb | 17 ++++++------- lib/generators/blazer/templates/install.rb.tt | 1 + test/internal/config/blazer.yml | 4 ++++ test/internal/config/routes.rb | 2 ++ test/internal/db/schema.rb | 1 + test/queries_test.rb | 24 +++++++++++++++---- 8 files changed, 47 insertions(+), 19 deletions(-) diff --git a/app/controllers/blazer/queries_controller.rb b/app/controllers/blazer/queries_controller.rb index 9536fbe1c..c81a31349 100644 --- a/app/controllers/blazer/queries_controller.rb +++ b/app/controllers/blazer/queries_controller.rb @@ -79,11 +79,12 @@ def edit end def share - if params[:token] && params[:query_id] && params[:token] == Blazer.sharing.query_token(params[:query_id]) - run - else - render_forbidden - end + return render_forbidden unless params[:token] && params[:query_id] + + @query = Query.find_by(id: params[:query_id]) if params[:query_id] + return render_forbidden unless @query.correct_token?(params[:token]) + + run end def run diff --git a/app/models/blazer/query.rb b/app/models/blazer/query.rb index f51fd8f96..c6b444a2c 100644 --- a/app/models/blazer/query.rb +++ b/app/models/blazer/query.rb @@ -1,5 +1,7 @@ module Blazer class Query < Record + has_secure_token :secret_token, length: 36 + belongs_to :creator, optional: true, class_name: Blazer.user_class.to_s if Blazer.user_class has_many :checks, dependent: :destroy has_many :dashboard_queries, dependent: :destroy @@ -15,6 +17,10 @@ def to_param [id, name].compact.join("-").gsub("'", "").parameterize end + def correct_token?(token) + ActiveSupport::SecurityUtils.secure_compare(secret_token, token) + end + def friendly_name name.to_s.sub(/\A[#\*]/, "").gsub(/\[.+\]/, "").strip end diff --git a/lib/blazer/sharing.rb b/lib/blazer/sharing.rb index b514a21a9..248a8d4f9 100644 --- a/lib/blazer/sharing.rb +++ b/lib/blazer/sharing.rb @@ -1,10 +1,10 @@ module Blazer class Sharing - attr_accessor :api_key, :path + attr_accessor :path, :enabled - def initialize(api_key: ENV.fetch('BLAZER_DOWNLOAD_API_KEY', nil), path: '/blazer_share') - @api_key = api_key + def initialize(enabled: false, path: '/blazer_share') @path = path.sub(/\/$/, '') # Strip trailing / + @enabled = enabled end def route_path @@ -15,16 +15,13 @@ def to_controller 'blazer/queries#share' end - def query_token(query_id) - Digest::SHA1.hexdigest("#{query_id}-#{api_key}") - end - def enabled? - api_key.present? + enabled end - def share_path(query_id, format: nil) - "#{path}/#{query_token(query_id)}/#{query_id}#{".#{format}" if format}" + def share_path(query_id, format: nil, token: nil) + query = Query.find(query_id) + "#{path}/#{token}/#{query_id}#{".#{format}" if format}" end def url_for(query_id, current_url, format: 'csv') diff --git a/lib/generators/blazer/templates/install.rb.tt b/lib/generators/blazer/templates/install.rb.tt index bf1999303..22220aea0 100644 --- a/lib/generators/blazer/templates/install.rb.tt +++ b/lib/generators/blazer/templates/install.rb.tt @@ -5,6 +5,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version t.string :name t.text :description t.text :statement + t.text :secret_token t.string :data_source t.string :status t.timestamps null: false diff --git a/test/internal/config/blazer.yml b/test/internal/config/blazer.yml index 07c7b09d6..d3b55d652 100644 --- a/test/internal/config/blazer.yml +++ b/test/internal/config/blazer.yml @@ -161,3 +161,7 @@ uploads: url: postgres://localhost/blazer_test schema: uploads data_source: main + +sharing: + path: /blazer_share + enabled: true diff --git a/test/internal/config/routes.rb b/test/internal/config/routes.rb index fc4c62b3e..d0932527b 100644 --- a/test/internal/config/routes.rb +++ b/test/internal/config/routes.rb @@ -1,3 +1,5 @@ Rails.application.routes.draw do mount Blazer::Engine, at: "/" + + get Blazer.sharing.route_path, to: Blazer.sharing.to_controller, as: :share_query if Blazer.sharing.enabled? end diff --git a/test/internal/db/schema.rb b/test/internal/db/schema.rb index 6d6b43035..5ef6b772a 100644 --- a/test/internal/db/schema.rb +++ b/test/internal/db/schema.rb @@ -4,6 +4,7 @@ t.string :name t.text :description t.text :statement + t.text :secret_token t.string :data_source t.string :status t.timestamps null: false diff --git a/test/queries_test.rb b/test/queries_test.rb index bd78a219f..3cf99f387 100644 --- a/test/queries_test.rb +++ b/test/queries_test.rb @@ -76,6 +76,22 @@ def test_variables_time_range assert_match "daterangepicker", response.body end + def test_correct_token + query = create_query(statement: "SELECT 1") + get share_query_path(query.id, token: query.secret_token, format: 'csv') + + assert_response :success + assert_equal "text/csv", response.content_type + end + + def test_incorrect_token + query = create_query(statement: "SELECT 1") + get share_query_path(query.id, token: "x") + + assert_response :forbidden + assert_match "Access denied", response.body + end + def test_variable_defaults query = create_query(statement: "SELECT {default_var}") get blazer.query_path(query) @@ -108,12 +124,12 @@ def test_csv end def test_share - Blazer.sharing.api_key = "123" query = create_query - get blazer.query_share_path(query_id: query.id, token: Digest::SHA1.hexdigest("#{query.id}-123"), format: 'csv') + assert query.secret_token + + get blazer.query_share_path(query_id: query.id, token: query.secret_token, format: 'csv') + assert_response :success - assert_match query.name, response.body - Blazer.sharing.api_key = nil end def test_url From 6085eb02d73546a5ad3485f7d032b093be94e480 Mon Sep 17 00:00:00 2001 From: Achilleas Buisman Date: Mon, 11 Sep 2023 23:10:36 +0200 Subject: [PATCH 09/10] Check for utf-8 in type --- test/queries_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/queries_test.rb b/test/queries_test.rb index 1f71acd6c..dc93111e6 100644 --- a/test/queries_test.rb +++ b/test/queries_test.rb @@ -87,7 +87,7 @@ def test_correct_token get share_query_path(query.id, token: query.secret_token, format: 'csv') assert_response :success - assert_equal "text/csv", response.content_type + assert_equal "text/csv; charset=utf-8", response.content_type end def test_incorrect_token From f73dad8584cc2cf4eaebadd4206e87c63b709478 Mon Sep 17 00:00:00 2001 From: Achilleas Buisman Date: Tue, 12 Sep 2023 09:24:54 +0200 Subject: [PATCH 10/10] Fix generated share link --- lib/blazer/sharing.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/blazer/sharing.rb b/lib/blazer/sharing.rb index 248a8d4f9..f4f62885e 100644 --- a/lib/blazer/sharing.rb +++ b/lib/blazer/sharing.rb @@ -19,9 +19,9 @@ def enabled? enabled end - def share_path(query_id, format: nil, token: nil) + def share_path(query_id, format: nil) query = Query.find(query_id) - "#{path}/#{token}/#{query_id}#{".#{format}" if format}" + "#{path}/#{query.secret_token}/#{query_id}#{".#{format}" if format}" end def url_for(query_id, current_url, format: 'csv')