From 2589bef1f21b7c84e4ceacdf5cd2c620bdedb111 Mon Sep 17 00:00:00 2001
From: Jesse <jesse.whitehouse@databricks.com>
Date: Tue, 23 Nov 2021 18:43:48 -0600
Subject: [PATCH] Update V10 release branch to 10.1 (#5655)

* Add support for Firebolt Database (#5606)

* Fixes issue #5622 (#5623)

* Update Readme to reflect Firebolt data source (#5649)

* Speed up BigQuery schema fetching (#5632)

New method improves schema fetching by as much as 98% on larger schemas

* Merge pull request from GHSA-vhc7-w7r8-8m34

* WIP: break the flask_oauthlib behavior

* Refactor google-oauth to use cryptographic state.

* Clean up comments

* Fix: tests didn't pass because of the scope issues.

Moved outside the create_blueprint method because this does not depend
on the Authlib object.

* Apply Arik's fixes. Tests pass.

* Merge pull request from GHSA-g8xr-f424-h2rv

* Merge pull request from GHSA-fcpv-hgq6-87h7

* Update changelog to incorporate security fixes and #5632 & #5606 (#5654)

* Update changelog to incorporate security fixes and #5632 & #5606

* Added reference to sqlite fix

* Bump to V10.1

* Missed package-lock.json on the first pass

* Add a REDASH_COOKIE_SECRET for circleci

* Revert "Add a REDASH_COOKIE_SECRET for circleci"

This reverts commit 45766364e82327d80d8bc0bcc5fd7f4733291156.

Moves config to the correct compose files

* Move advocate to core requirements.txt file

[debugging circleci failures]

Co-authored-by: rajeshSigmoid <89909168+rajeshSigmoid@users.noreply.github.com>
Co-authored-by: Aratrik Pal <44343120+AP2008@users.noreply.github.com>
Co-authored-by: rajeshmauryasde <rajeshk@sigmoidanalytics.com>
Co-authored-by: Katsuya Shimabukuro <katsu.generation.888@gmail.com>
---
 .circleci/docker-compose.circle.yml           |   1 +
 .circleci/docker-compose.cypress.yml          |   1 +
 CHANGELOG.md                                  |  14 ++
 README.md                                     |   1 +
 .../app/assets/images/db-logos/firebolt.png   | Bin 0 -> 12706 bytes
 docker-compose.yml                            |   3 +
 package-lock.json                             |   2 +-
 package.json                                  |   2 +-
 redash/__init__.py                            |   2 +-
 redash/authentication/__init__.py             |   8 +-
 redash/authentication/google_oauth.py         | 181 +++++++++---------
 redash/query_runner/__init__.py               |  18 +-
 redash/query_runner/big_query.py              |  58 +++---
 redash/query_runner/csv.py                    |  12 +-
 redash/query_runner/excel.py                  |  11 +-
 redash/query_runner/firebolt.py               |  94 +++++++++
 redash/query_runner/json_ds.py                |   7 +-
 redash/query_runner/sqlite.py                 |   2 +-
 redash/settings/__init__.py                   |   9 +-
 redash/utils/requests_session.py              |  10 +-
 requirements.txt                              |   6 +-
 requirements_all_ds.txt                       |   1 +
 tests/query_runner/test_http.py               |   6 +-
 23 files changed, 286 insertions(+), 163 deletions(-)
 create mode 100644 client/app/assets/images/db-logos/firebolt.png
 create mode 100644 redash/query_runner/firebolt.py

diff --git a/.circleci/docker-compose.circle.yml b/.circleci/docker-compose.circle.yml
index 84ef76a823..f607e7534a 100644
--- a/.circleci/docker-compose.circle.yml
+++ b/.circleci/docker-compose.circle.yml
@@ -13,6 +13,7 @@ services:
       REDASH_LOG_LEVEL: "INFO"
       REDASH_REDIS_URL: "redis://redis:6379/0"
       REDASH_DATABASE_URL: "postgresql://postgres@postgres/postgres"
+      REDASH_COOKIE_SECRET: "2H9gNG9obnAQ9qnR9BDTQUph6CbXKCzF"
   redis:
     image: redis:3.0-alpine
     restart: unless-stopped
diff --git a/.circleci/docker-compose.cypress.yml b/.circleci/docker-compose.cypress.yml
index d265c85744..da57925f11 100644
--- a/.circleci/docker-compose.cypress.yml
+++ b/.circleci/docker-compose.cypress.yml
@@ -12,6 +12,7 @@ x-redash-environment: &redash-environment
   REDASH_DATABASE_URL: "postgresql://postgres@postgres/postgres"
   REDASH_RATELIMIT_ENABLED: "false"
   REDASH_ENFORCE_CSRF: "true"
+  REDASH_COOKIE_SECRET: "2H9gNG9obnAQ9qnR9BDTQUph6CbXKCzF"
 services:
   server:
     <<: *redash-service
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9303f30fd0..9c53e7b7f4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,19 @@
 # Change Log
 
+## V10.1.0 - 2021-11-23
+
+This release includes patches for three security vulnerabilities:
+
+- Insecure default configuration affects installations where REDASH_COOKIE_SECRET is not set explicitly (CVE-2021-41192)
+- SSRF vulnerability affects installations that enabled URL-loading data sources (CVE-2021-43780)
+- Incorrect usage of state parameter in OAuth client code affects installations where Google Login is enabled (CVE-2021-43777)
+
+And a couple features that didn't merge in time for 10.0.0
+
+- Big Query: Speed up schema loading (#5632)
+- Add support for Firebolt data source (#5606)
+- Fix: Loading schema for Sqlite DB with "Order" column name fails (#5623)
+
 ## v10.0.0 - 2021-10-01
 
 A few changes were merged during the V10 beta period.
diff --git a/README.md b/README.md
index 46d44d203d..a32aeabbd1 100644
--- a/README.md
+++ b/README.md
@@ -43,6 +43,7 @@ Redash supports more than 35 SQL and NoSQL [data sources](https://redash.io/help
 - DB2 by IBM
 - Druid
 - Elasticsearch
+- Firebolt
 - Google Analytics
 - Google BigQuery
 - Google Spreadsheets
diff --git a/client/app/assets/images/db-logos/firebolt.png b/client/app/assets/images/db-logos/firebolt.png
new file mode 100644
index 0000000000000000000000000000000000000000..7b6c02a66df850741955404ee5e6ae1ef854ce13
GIT binary patch
literal 12706
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
LG!WHt=Ar)wW^f6M

literal 0
HcmV?d00001

diff --git a/docker-compose.yml b/docker-compose.yml
index 82bcd245fb..fdfcdd1a5d 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -8,6 +8,8 @@ x-redash-service: &redash-service
       skip_frontend_build: "true"
   volumes:
     - .:/app
+  env_file:
+    - .env
 x-redash-environment: &redash-environment
   REDASH_LOG_LEVEL: "INFO"
   REDASH_REDIS_URL: "redis://redis:6379/0"
@@ -16,6 +18,7 @@ x-redash-environment: &redash-environment
   REDASH_MAIL_DEFAULT_SENDER: "redash@example.com"
   REDASH_MAIL_SERVER: "email"
   REDASH_ENFORCE_CSRF: "true"
+  # Set secret keys in the .env file
 services:
   server:
     <<: *redash-service
diff --git a/package-lock.json b/package-lock.json
index 385147bf7d..5276ba3fe3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "redash-client",
-  "version": "10.0.0",
+  "version": "10.1.0",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
diff --git a/package.json b/package.json
index ddea524511..1f75258990 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "redash-client",
-  "version": "10.0.0",
+  "version": "10.1.0",
   "description": "The frontend part of Redash.",
   "main": "index.js",
   "scripts": {
diff --git a/redash/__init__.py b/redash/__init__.py
index 6b70e7857f..42724fe076 100644
--- a/redash/__init__.py
+++ b/redash/__init__.py
@@ -15,7 +15,7 @@
 from .query_runner import import_query_runners
 from .destinations import import_destinations
 
-__version__ = "10.0.0"
+__version__ = "10.1.0"
 
 
 if os.environ.get("REMOTE_DEBUG"):
diff --git a/redash/authentication/__init__.py b/redash/authentication/__init__.py
index 94bf53bea1..f06cd3cdb2 100644
--- a/redash/authentication/__init__.py
+++ b/redash/authentication/__init__.py
@@ -243,12 +243,13 @@ def logout_and_redirect_to_index():
 
 def init_app(app):
     from redash.authentication import (
-        google_oauth,
         saml_auth,
         remote_user_auth,
         ldap_auth,
     )
 
+    from redash.authentication.google_oauth import create_google_oauth_blueprint
+
     login_manager.init_app(app)
     login_manager.anonymous_user = models.AnonymousUser
     login_manager.REMEMBER_COOKIE_DURATION = settings.REMEMBER_COOKIE_DURATION
@@ -259,8 +260,9 @@ def extend_session():
         app.permanent_session_lifetime = timedelta(seconds=settings.SESSION_EXPIRY_TIME)
 
     from redash.security import csrf
-    for auth in [google_oauth, saml_auth, remote_user_auth, ldap_auth]:
-        blueprint = auth.blueprint
+
+    # Authlib's flask oauth client requires a Flask app to initialize
+    for blueprint in [create_google_oauth_blueprint(app), saml_auth.blueprint, remote_user_auth.blueprint, ldap_auth.blueprint, ]:
         csrf.exempt(blueprint)
         app.register_blueprint(blueprint)
 
diff --git a/redash/authentication/google_oauth.py b/redash/authentication/google_oauth.py
index 59d49ef90e..a1f58d3fcf 100644
--- a/redash/authentication/google_oauth.py
+++ b/redash/authentication/google_oauth.py
@@ -1,7 +1,7 @@
 import logging
 import requests
 from flask import redirect, url_for, Blueprint, flash, request, session
-from flask_oauthlib.client import OAuth
+
 
 from redash import models, settings
 from redash.authentication import (
@@ -11,42 +11,7 @@
 )
 from redash.authentication.org_resolving import current_org
 
-logger = logging.getLogger("google_oauth")
-
-oauth = OAuth()
-blueprint = Blueprint("google_oauth", __name__)
-
-
-def google_remote_app():
-    if "google" not in oauth.remote_apps:
-        oauth.remote_app(
-            "google",
-            base_url="https://www.google.com/accounts/",
-            authorize_url="https://accounts.google.com/o/oauth2/auth?prompt=select_account+consent",
-            request_token_url=None,
-            request_token_params={
-                "scope": "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile"
-            },
-            access_token_url="https://accounts.google.com/o/oauth2/token",
-            access_token_method="POST",
-            consumer_key=settings.GOOGLE_CLIENT_ID,
-            consumer_secret=settings.GOOGLE_CLIENT_SECRET,
-        )
-
-    return oauth.google
-
-
-def get_user_profile(access_token):
-    headers = {"Authorization": "OAuth {}".format(access_token)}
-    response = requests.get(
-        "https://www.googleapis.com/oauth2/v1/userinfo", headers=headers
-    )
-
-    if response.status_code == 401:
-        logger.warning("Failed getting user profile (response code 401).")
-        return None
-
-    return response.json()
+from authlib.integrations.flask_client import OAuth
 
 
 def verify_profile(org, profile):
@@ -65,60 +30,102 @@ def verify_profile(org, profile):
     return False
 
 
-@blueprint.route("/<org_slug>/oauth/google", endpoint="authorize_org")
-def org_login(org_slug):
-    session["org_slug"] = current_org.slug
-    return redirect(url_for(".authorize", next=request.args.get("next", None)))
+def create_google_oauth_blueprint(app):
+    oauth = OAuth(app)
 
+    logger = logging.getLogger("google_oauth")
+    blueprint = Blueprint("google_oauth", __name__)
 
-@blueprint.route("/oauth/google", endpoint="authorize")
-def login():
-    callback = url_for(".callback", _external=True)
-    next_path = request.args.get(
-        "next", url_for("redash.index", org_slug=session.get("org_slug"))
+    CONF_URL = "https://accounts.google.com/.well-known/openid-configuration"
+    oauth = OAuth(app)
+    oauth.register(
+        name="google",
+        server_metadata_url=CONF_URL,
+        client_kwargs={"scope": "openid email profile"},
     )
-    logger.debug("Callback url: %s", callback)
-    logger.debug("Next is: %s", next_path)
-    return google_remote_app().authorize(callback=callback, state=next_path)
-
-
-@blueprint.route("/oauth/google_callback", endpoint="callback")
-def authorized():
-    resp = google_remote_app().authorized_response()
-    access_token = resp["access_token"]
-
-    if access_token is None:
-        logger.warning("Access token missing in call back request.")
-        flash("Validation error. Please retry.")
-        return redirect(url_for("redash.login"))
-
-    profile = get_user_profile(access_token)
-    if profile is None:
-        flash("Validation error. Please retry.")
-        return redirect(url_for("redash.login"))
-
-    if "org_slug" in session:
-        org = models.Organization.get_by_slug(session.pop("org_slug"))
-    else:
-        org = current_org
-
-    if not verify_profile(org, profile):
-        logger.warning(
-            "User tried to login with unauthorized domain name: %s (org: %s)",
-            profile["email"],
-            org,
+
+    def get_user_profile(access_token):
+        headers = {"Authorization": "OAuth {}".format(access_token)}
+        response = requests.get(
+            "https://www.googleapis.com/oauth2/v1/userinfo", headers=headers
         )
-        flash("Your Google Apps account ({}) isn't allowed.".format(profile["email"]))
-        return redirect(url_for("redash.login", org_slug=org.slug))
 
-    picture_url = "%s?sz=40" % profile["picture"]
-    user = create_and_login_user(org, profile["name"], profile["email"], picture_url)
-    if user is None:
-        return logout_and_redirect_to_index()
+        if response.status_code == 401:
+            logger.warning("Failed getting user profile (response code 401).")
+            return None
 
-    unsafe_next_path = request.args.get("state") or url_for(
-        "redash.index", org_slug=org.slug
-    )
-    next_path = get_next_path(unsafe_next_path)
+        return response.json()
+
+    @blueprint.route("/<org_slug>/oauth/google", endpoint="authorize_org")
+    def org_login(org_slug):
+        session["org_slug"] = current_org.slug
+        return redirect(url_for(".authorize", next=request.args.get("next", None)))
+
+    @blueprint.route("/oauth/google", endpoint="authorize")
+    def login():
+
+        redirect_uri = url_for(".callback", _external=True)
+
+        next_path = request.args.get(
+            "next", url_for("redash.index", org_slug=session.get("org_slug"))
+        )
+        logger.debug("Callback url: %s", redirect_uri)
+        logger.debug("Next is: %s", next_path)
+
+        session["next_url"] = next_path
+
+        return oauth.google.authorize_redirect(redirect_uri)
+
+    @blueprint.route("/oauth/google_callback", endpoint="callback")
+    def authorized():
+
+        logger.debug("Authorized user inbound")
+
+        resp = oauth.google.authorize_access_token()
+        user = resp.get("userinfo")
+        if user:
+            session["user"] = user
+
+        access_token = resp["access_token"]
+
+        if access_token is None:
+            logger.warning("Access token missing in call back request.")
+            flash("Validation error. Please retry.")
+            return redirect(url_for("redash.login"))
+
+        profile = get_user_profile(access_token)
+        if profile is None:
+            flash("Validation error. Please retry.")
+            return redirect(url_for("redash.login"))
+
+        if "org_slug" in session:
+            org = models.Organization.get_by_slug(session.pop("org_slug"))
+        else:
+            org = current_org
+
+        if not verify_profile(org, profile):
+            logger.warning(
+                "User tried to login with unauthorized domain name: %s (org: %s)",
+                profile["email"],
+                org,
+            )
+            flash(
+                "Your Google Apps account ({}) isn't allowed.".format(profile["email"])
+            )
+            return redirect(url_for("redash.login", org_slug=org.slug))
+
+        picture_url = "%s?sz=40" % profile["picture"]
+        user = create_and_login_user(
+            org, profile["name"], profile["email"], picture_url
+        )
+        if user is None:
+            return logout_and_redirect_to_index()
+
+        unsafe_next_path = session.get("next_url") or url_for(
+            "redash.index", org_slug=org.slug
+        )
+        next_path = get_next_path(unsafe_next_path)
+
+        return redirect(next_path)
 
-    return redirect(next_path)
+    return blueprint
diff --git a/redash/query_runner/__init__.py b/redash/query_runner/__init__.py
index 66dfa6e437..6f8ef389b1 100644
--- a/redash/query_runner/__init__.py
+++ b/redash/query_runner/__init__.py
@@ -13,7 +13,8 @@
 from redash.utils import json_loads, query_is_select_no_limit, add_limit_to_query
 from rq.timeouts import JobTimeoutException
 
-from redash.utils.requests_session import requests, requests_session
+from redash.utils.requests_session import requests_or_advocate, requests_session, UnacceptableAddressException
+
 
 logger = logging.getLogger(__name__)
 
@@ -236,12 +237,6 @@ def apply_auto_limit(self, query_text, should_apply_auto_limit):
             return query_text
 
 
-def is_private_address(url):
-    hostname = urlparse(url).hostname
-    ip_address = socket.gethostbyname(hostname)
-    return ipaddress.ip_address(text_type(ip_address)).is_private
-
-
 class BaseHTTPQueryRunner(BaseQueryRunner):
     should_annotate_query = False
     response_error = "Endpoint returned unexpected status code"
@@ -285,8 +280,6 @@ def get_auth(self):
             return None
 
     def get_response(self, url, auth=None, http_method="get", **kwargs):
-        if is_private_address(url) and settings.ENFORCE_PRIVATE_ADDRESS_BLOCK:
-            raise Exception("Can't query private addresses.")
 
         # Get authentication values if not given
         if auth is None:
@@ -307,12 +300,15 @@ def get_response(self, url, auth=None, http_method="get", **kwargs):
             if response.status_code != 200:
                 error = "{} ({}).".format(self.response_error, response.status_code)
 
-        except requests.HTTPError as exc:
+        except requests_or_advocate.HTTPError as exc:
             logger.exception(exc)
             error = "Failed to execute query. " "Return Code: {} Reason: {}".format(
                 response.status_code, response.text
             )
-        except requests.RequestException as exc:
+        except UnacceptableAddressException as exc:
+            logger.exception(exc)
+            error = "Can't query private addresses."
+        except requests_or_advocate.RequestException as exc:
             # Catch all other requests exceptions and return the error.
             logger.exception(exc)
             error = str(exc)
diff --git a/redash/query_runner/big_query.py b/redash/query_runner/big_query.py
index a592166308..eddbde199e 100644
--- a/redash/query_runner/big_query.py
+++ b/redash/query_runner/big_query.py
@@ -268,41 +268,33 @@ def get_schema(self, get_stats=False):
         service = self._get_bigquery_service()
         project_id = self._get_project_id()
         datasets = service.datasets().list(projectId=project_id).execute()
-        schema = []
-        for dataset in datasets.get("datasets", []):
-            dataset_id = dataset["datasetReference"]["datasetId"]
-            tables = (
-                service.tables()
-                .list(projectId=project_id, datasetId=dataset_id)
-                .execute()
-            )
-            while True:
-                for table in tables.get("tables", []):
-                    table_data = (
-                        service.tables()
-                        .get(
-                            projectId=project_id,
-                            datasetId=dataset_id,
-                            tableId=table["tableReference"]["tableId"],
-                        )
-                        .execute()
-                    )
-                    table_schema = self._get_columns_schema(table_data)
-                    schema.append(table_schema)
 
-                next_token = tables.get("nextPageToken", None)
-                if next_token is None:
-                    break
+        query_base = """
+        SELECT table_schema, table_name, column_name
+        FROM `{dataset_id}`.INFORMATION_SCHEMA.COLUMNS
+        WHERE table_schema NOT IN ('information_schema')
+        """
 
-                tables = (
-                    service.tables()
-                    .list(
-                        projectId=project_id, datasetId=dataset_id, pageToken=next_token
-                    )
-                    .execute()
-                )
-
-        return schema
+        schema = {}
+        queries = []
+        for dataset in datasets.get("datasets", []):
+            dataset_id = dataset["datasetReference"]["datasetId"]
+            query = query_base.format(dataset_id=dataset_id)
+            queries.append(query)
+
+        query = '\nUNION ALL\n'.join(queries)
+        results, error = self.run_query(query, None)
+        if error is not None:
+            raise Exception("Failed getting schema.")
+
+        results = json_loads(results)
+        for row in results["rows"]:
+            table_name = "{0}.{1}".format(row["table_schema"], row["table_name"])
+            if table_name not in schema:
+                schema[table_name] = {"name": table_name, "columns": []}
+            schema[table_name]["columns"].append(row["column_name"])
+
+        return list(schema.values())
 
     def run_query(self, query, user):
         logger.debug("BigQuery got query: %s", query)
diff --git a/redash/query_runner/csv.py b/redash/query_runner/csv.py
index 8514868e2d..22aa148d57 100644
--- a/redash/query_runner/csv.py
+++ b/redash/query_runner/csv.py
@@ -1,9 +1,9 @@
 import logging
 import yaml
-import requests
 import io
 
-from redash import settings
+from redash.utils.requests_session import requests_or_advocate, UnacceptableAddressException
+
 from redash.query_runner import *
 from redash.utils import json_dumps
 
@@ -52,14 +52,11 @@ def run_query(self, query, user):
             args.pop('url', None)
             ua = args['user-agent']
             args.pop('user-agent', None)
-
-            if is_private_address(path) and settings.ENFORCE_PRIVATE_ADDRESS_BLOCK:
-                raise Exception("Can't query private addresses.")
         except:
             pass
 
         try:
-            response = requests.get(url=path, headers={"User-agent": ua})
+            response = requests_or_advocate.get(url=path, headers={"User-agent": ua})
             workbook = pd.read_csv(io.BytesIO(response.content),sep=",", **args)
 
             df = workbook.copy()
@@ -88,6 +85,9 @@ def run_query(self, query, user):
         except KeyboardInterrupt:
             error = "Query cancelled by user."
             json_data = None
+        except UnacceptableAddressException:
+            error = "Can't query private addresses."
+            json_data = None
         except Exception as e:
             error = "Error reading {0}. {1}".format(path, str(e))
             json_data = None
diff --git a/redash/query_runner/excel.py b/redash/query_runner/excel.py
index a58c23ac97..792a821bcd 100644
--- a/redash/query_runner/excel.py
+++ b/redash/query_runner/excel.py
@@ -1,8 +1,8 @@
 import logging
 import yaml
-import requests
 
-from redash import settings
+from redash.utils.requests_session import requests_or_advocate, UnacceptableAddressException
+
 from redash.query_runner import *
 from redash.utils import json_dumps
 
@@ -49,13 +49,11 @@ def run_query(self, query, user):
             ua = args['user-agent']
             args.pop('user-agent', None)
 
-            if is_private_address(path) and settings.ENFORCE_PRIVATE_ADDRESS_BLOCK:
-                raise Exception("Can't query private addresses.")
         except:
             pass
 
         try:
-            response = requests.get(url=path, headers={"User-agent": ua})
+            response = requests_or_advocate.get(url=path, headers={"User-agent": ua})
             workbook = pd.read_excel(response.content, **args)
 
             df = workbook.copy()
@@ -84,6 +82,9 @@ def run_query(self, query, user):
         except KeyboardInterrupt:
             error = "Query cancelled by user."
             json_data = None
+        except UnacceptableAddressException:
+            error = "Can't query private addresses."
+            json_data = None
         except Exception as e:
             error = "Error reading {0}. {1}".format(path, str(e))
             json_data = None
diff --git a/redash/query_runner/firebolt.py b/redash/query_runner/firebolt.py
new file mode 100644
index 0000000000..761d1e644f
--- /dev/null
+++ b/redash/query_runner/firebolt.py
@@ -0,0 +1,94 @@
+try:
+    from firebolt_db.firebolt_connector import Connection
+    enabled = True
+except ImportError:
+    enabled = False
+
+from redash.query_runner import BaseQueryRunner, register
+from redash.query_runner import TYPE_STRING, TYPE_INTEGER, TYPE_BOOLEAN
+from redash.utils import json_dumps, json_loads
+
+TYPES_MAP = {1: TYPE_STRING, 2: TYPE_INTEGER, 3: TYPE_BOOLEAN}
+
+
+class Firebolt(BaseQueryRunner):
+    noop_query = "SELECT 1"
+
+    @classmethod
+    def configuration_schema(cls):
+        return {
+            "type": "object",
+            "properties": {
+                "host": {"type": "string", "default": "localhost"},
+                "port": {"type": "string", "default": 8123},
+                "DB": {"type": "string"},
+                "user": {"type": "string"},
+                "password": {"type": "string"},
+            },
+            "order": ["host", "port", "user", "password", "DB"],
+            "required": ["user","password"],
+            "secret": ["password"],
+        }
+
+    @classmethod
+    def enabled(cls):
+        return enabled
+
+    def run_query(self, query, user):
+        connection = Connection(
+            host=self.configuration["host"],
+            port=self.configuration["port"],
+            username=(self.configuration.get("user") or None),
+            password=(self.configuration.get("password") or None),
+            db_name=(self.configuration.get("DB") or None),
+        )
+
+        cursor = connection.cursor()
+
+        try:
+            cursor.execute(query)
+            columns = self.fetch_columns(
+                [(i[0], TYPES_MAP.get(i[1], None)) for i in cursor.description]
+            )
+            rows = [
+                dict(zip((column["name"] for column in columns), row)) for row in cursor
+            ]
+
+            data = {"columns": columns, "rows": rows}
+            error = None
+            json_data = json_dumps(data)
+        finally:
+            connection.close()
+
+        return json_data, error
+
+
+    def get_schema(self, get_stats=False):
+        query = """
+        SELECT TABLE_SCHEMA,
+               TABLE_NAME,
+               COLUMN_NAME
+        FROM INFORMATION_SCHEMA.COLUMNS
+        WHERE TABLE_SCHEMA <> 'INFORMATION_SCHEMA'
+        """
+
+        results, error = self.run_query(query, None)
+
+        if error is not None:
+            raise Exception("Failed getting schema.")
+
+        schema = {}
+        results = json_loads(results)
+
+        for row in results["rows"]:
+            table_name = "{}.{}".format(row["table_schema"], row["table_name"])
+
+            if table_name not in schema:
+                schema[table_name] = {"name": table_name, "columns": []}
+
+            schema[table_name]["columns"].append(row["column_name"])
+
+        return list(schema.values())
+
+
+register(Firebolt)
diff --git a/redash/query_runner/json_ds.py b/redash/query_runner/json_ds.py
index ed8562bcc4..a6cf42eb5e 100644
--- a/redash/query_runner/json_ds.py
+++ b/redash/query_runner/json_ds.py
@@ -2,7 +2,9 @@
 import yaml
 import datetime
 from funcy import compact, project
-from redash import settings
+
+from redash.utils.requests_session import requests_or_advocate, UnacceptableAddressException
+
 from redash.utils import json_dumps
 from redash.query_runner import (
     BaseHTTPQueryRunner,
@@ -12,7 +14,6 @@
     TYPE_FLOAT,
     TYPE_INTEGER,
     TYPE_STRING,
-    is_private_address,
 )
 
 
@@ -163,8 +164,6 @@ def run_query(self, query, user):
         if "url" not in query:
             raise QueryParseError("Query must include 'url' option.")
 
-        if is_private_address(query["url"]) and settings.ENFORCE_PRIVATE_ADDRESS_BLOCK:
-            raise Exception("Can't query private addresses.")
 
         method = query.get("method", "get")
         request_options = project(query, ("params", "headers", "data", "auth", "json"))
diff --git a/redash/query_runner/sqlite.py b/redash/query_runner/sqlite.py
index ffee42bf7f..d056dff98f 100644
--- a/redash/query_runner/sqlite.py
+++ b/redash/query_runner/sqlite.py
@@ -29,7 +29,7 @@ def __init__(self, configuration):
 
     def _get_tables(self, schema):
         query_table = "select tbl_name from sqlite_master where type='table'"
-        query_columns = "PRAGMA table_info(%s)"
+        query_columns = "PRAGMA table_info(\"%s\")"
 
         results, error = self.run_query(query_table, None)
 
diff --git a/redash/settings/__init__.py b/redash/settings/__init__.py
index 7b2c967d03..4879ddb309 100644
--- a/redash/settings/__init__.py
+++ b/redash/settings/__init__.py
@@ -64,7 +64,11 @@
 )
 
 # The secret key to use in the Flask app for various cryptographic features
-SECRET_KEY = os.environ.get("REDASH_COOKIE_SECRET", "c292a0a3aa32397cdb050e233733900f")
+SECRET_KEY = os.environ.get("REDASH_COOKIE_SECRET")
+
+if SECRET_KEY is None:
+    raise Exception("You must set the REDASH_COOKIE_SECRET environment variable. Visit http://redash.io/help/open-source/admin-guide/secrets for more information.")
+
 # The secret key to use when encrypting data source options
 DATASOURCE_SECRET_KEY = os.environ.get("REDASH_SECRET_KEY", SECRET_KEY)
 
@@ -382,7 +386,8 @@ def email_server_is_configured():
     "redash.query_runner.corporate_memory",
     "redash.query_runner.sparql_endpoint",
     "redash.query_runner.excel",
-    "redash.query_runner.csv"
+    "redash.query_runner.csv",
+    "redash.query_runner.firebolt"
 ]
 
 enabled_query_runners = array_from_string(
diff --git a/redash/utils/requests_session.py b/redash/utils/requests_session.py
index e59731b0e6..f38133a9a4 100644
--- a/redash/utils/requests_session.py
+++ b/redash/utils/requests_session.py
@@ -1,8 +1,14 @@
-import requests
 from redash import settings
 
+from advocate.exceptions import UnacceptableAddressException
+if settings.ENFORCE_PRIVATE_ADDRESS_BLOCK:
+    import advocate as requests_or_advocate
+else:
+    import requests as requests_or_advocate
 
-class ConfiguredSession(requests.Session):
+
+
+class ConfiguredSession(requests_or_advocate.Session):
     def request(self, *args, **kwargs):
         if not settings.REQUESTS_ALLOW_REDIRECTS:
             kwargs.update({"allow_redirects": False})
diff --git a/requirements.txt b/requirements.txt
index 62e64f3a36..e5e5e01caa 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,9 +8,6 @@ httplib2==0.14.0
 wtforms==2.2.1
 Flask-RESTful==0.3.7
 Flask-Login==0.4.1
-Flask-OAuthLib==0.9.5
-# pin this until https://github.com/lepture/flask-oauthlib/pull/388 is released
-requests-oauthlib>=0.6.2,<1.2.0
 Flask-SQLAlchemy==2.4.1
 Flask-Migrate==2.5.2
 flask-mail==0.9.1
@@ -42,6 +39,7 @@ jsonschema==3.1.1
 RestrictedPython==5.0
 pysaml2==6.1.0
 pycrypto==2.6.1
+python-dotenv==0.19.2
 funcy==1.13
 sentry-sdk>=0.14.3,<0.15.0
 semver==2.8.1
@@ -67,3 +65,5 @@ werkzeug==0.16.1
 # Uncomment the requirement for ldap3 if using ldap.
 # It is not included by default because of the GPL license conflict.
 # ldap3==2.2.4
+Authlib==0.15.5
+advocate==1.0.0
\ No newline at end of file
diff --git a/requirements_all_ds.txt b/requirements_all_ds.txt
index 4bdbe9f474..9a3fcebbaa 100644
--- a/requirements_all_ds.txt
+++ b/requirements_all_ds.txt
@@ -40,3 +40,4 @@ trino~=0.305
 cmem-cmempy==21.2.3
 xlrd==2.0.1
 openpyxl==3.0.7
+firebolt-sqlalchemy
\ No newline at end of file
diff --git a/tests/query_runner/test_http.py b/tests/query_runner/test_http.py
index 00ab6500c2..1eb499eaae 100644
--- a/tests/query_runner/test_http.py
+++ b/tests/query_runner/test_http.py
@@ -1,7 +1,7 @@
 import mock
 from unittest import TestCase
 
-from redash.utils.requests_session import requests, ConfiguredSession
+from redash.utils.requests_session import requests_or_advocate, ConfiguredSession
 from redash.query_runner import BaseHTTPQueryRunner
 
 
@@ -84,7 +84,7 @@ def test_get_response_httperror_exception(self, mock_get):
         mock_response = mock.Mock()
         mock_response.status_code = 500
         mock_response.text = "Server Error"
-        http_error = requests.HTTPError()
+        http_error = requests_or_advocate.HTTPError()
         mock_response.raise_for_status.side_effect = http_error
         mock_get.return_value = mock_response
 
@@ -101,7 +101,7 @@ def test_get_response_requests_exception(self, mock_get):
         mock_response.status_code = 500
         mock_response.text = "Server Error"
         exception_message = "Some requests exception"
-        requests_exception = requests.RequestException(exception_message)
+        requests_exception = requests_or_advocate.RequestException(exception_message)
         mock_response.raise_for_status.side_effect = requests_exception
         mock_get.return_value = mock_response