Skip to content

Commit

Permalink
Update V10 release branch to 10.1 (#5655)
Browse files Browse the repository at this point in the history
* 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 4576636.

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>
  • Loading branch information
5 people authored Nov 24, 2021
1 parent 1c5ceec commit 2589bef
Show file tree
Hide file tree
Showing 23 changed files with 286 additions and 163 deletions.
1 change: 1 addition & 0 deletions .circleci/docker-compose.circle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .circleci/docker-compose.cypress.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Binary file added client/app/assets/images/db-logos/firebolt.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion redash/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand Down
8 changes: 5 additions & 3 deletions redash/authentication/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand Down
181 changes: 94 additions & 87 deletions redash/authentication/google_oauth.py
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -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):
Expand All @@ -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
18 changes: 7 additions & 11 deletions redash/query_runner/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down
Loading

0 comments on commit 2589bef

Please sign in to comment.