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