Compare commits

...

7 Commits

Author SHA1 Message Date
Arik Fraimovich
8c5e2dffc4 Bump version 2018-10-18 11:27:34 +03:00
YOSHIDA Katsuhiko
6f5a69f170 Preventing open redirection (#2906)
* Prevent open redirection attack

* Add redirection url after logging in test

* Sanitize url just before redirecting it

* Consider when next parameter is None
2018-10-18 11:26:34 +03:00
Alexey Korobkov
de0089ceb9 Add auth via JWT providers (#2768)
* authentication via JWT providers
* add support for IAP JWT auth
* remove jwt_auth Blueprint and /headers endpoint
* fix pep8: imports
2018-09-27 21:50:55 +03:00
Arik Fraimovich
fa92fec012 Update version 2018-09-27 21:49:08 +03:00
Jannis Leidel
2d86305852 Update to 3.1.26 to improve compatibility with Celery 4.x. (#2865) 2018-09-27 21:07:40 +03:00
Arik Fraimovich
d6ba42613b Add the release branch regex to the second pipeline 2018-09-24 12:39:28 +03:00
Arik Fraimovich
c91b73d9e3 Fix: CircleCI release branch regex 2018-09-24 12:37:32 +03:00
13 changed files with 197 additions and 23 deletions

View File

@@ -115,11 +115,11 @@ workflows:
branches:
only:
- master
- release/.*
- /release\/.*/
- build-docker-image:
requires:
- unit-tests
filters:
branches:
only:
- release/.*
- /release\/.*/

View File

@@ -1,6 +1,6 @@
{
"name": "redash-client",
"version": "5.0.0",
"version": "5.0.2",
"description": "The frontend part of Redash.",
"main": "index.js",
"scripts": {

View File

@@ -18,7 +18,7 @@ from redash.query_runner import import_query_runners
from redash.destinations import import_destinations
__version__ = '5.0.0'
__version__ = '5.0.2'
def setup_logging():

View File

@@ -6,8 +6,12 @@ import time
import logging
from flask import redirect, request, jsonify, url_for
from urlparse import urlsplit, urlunsplit
from werkzeug.exceptions import Unauthorized
from redash import models, settings
from redash.settings.organization import settings as org_settings
from redash.authentication import jwt_auth
from redash.authentication.org_resolving import current_org
from redash.tasks import record_event
@@ -48,6 +52,21 @@ def load_user(user_id):
return None
def request_loader(request):
user = None
if settings.AUTH_TYPE == 'hmac':
user = hmac_load_user_from_request(request)
elif settings.AUTH_TYPE == 'api_key':
user = api_key_load_user_from_request(request)
else:
logger.warning("Unknown authentication type ({}). Using default (HMAC).".format(settings.AUTH_TYPE))
user = hmac_load_user_from_request(request)
if org_settings['auth_jwt_login_enabled'] and user is None:
user = jwt_token_load_user_from_request(request)
return user
def hmac_load_user_from_request(request):
signature = request.args.get('signature')
expires = float(request.args.get('expires') or 0)
@@ -116,6 +135,40 @@ def api_key_load_user_from_request(request):
return user
def jwt_token_load_user_from_request(request):
org = current_org._get_current_object()
payload = None
if org_settings['auth_jwt_auth_cookie_name']:
jwt_token = request.cookies.get(org_settings['auth_jwt_auth_cookie_name'], None)
elif org_settings['auth_jwt_auth_header_name']:
jwt_token = request.headers.get(org_settings['auth_jwt_auth_header_name'], None)
else:
return None
if jwt_token:
payload, token_is_valid = jwt_auth.verify_jwt_token(
jwt_token,
expected_issuer=org_settings['auth_jwt_auth_issuer'],
expected_audience=org_settings['auth_jwt_auth_audience'],
algorithms=org_settings['auth_jwt_auth_algorithms'],
public_certs_url=org_settings['auth_jwt_auth_public_certs_url'],
)
if not token_is_valid:
raise Unauthorized('Invalid JWT token')
if not payload:
return
try:
user = models.User.get_by_email_and_org(payload['email'], org)
except models.NoResultFound:
user = create_and_login_user(current_org, payload['email'], payload['email'])
return user
def log_user_logged_in(app, user):
event = {
'org_id': current_org.id,
@@ -168,14 +221,7 @@ def setup_authentication(app):
app.register_blueprint(ldap_auth.blueprint)
user_logged_in.connect(log_user_logged_in)
if settings.AUTH_TYPE == 'hmac':
login_manager.request_loader(hmac_load_user_from_request)
elif settings.AUTH_TYPE == 'api_key':
login_manager.request_loader(api_key_load_user_from_request)
else:
logger.warning("Unknown authentication type ({}). Using default (HMAC).".format(settings.AUTH_TYPE))
login_manager.request_loader(hmac_load_user_from_request)
login_manager.request_loader(request_loader)
def create_and_login_user(org, name, email, picture=None):
@@ -197,3 +243,16 @@ def create_and_login_user(org, name, email, picture=None):
login_user(user_object, remember=True)
return user_object
def get_next_path(unsafe_next_path):
if not unsafe_next_path:
return ''
# Preventing open redirection attacks
parts = list(urlsplit(unsafe_next_path))
parts[0] = '' # clear scheme
parts[1] = '' # clear netloc
safe_next_path = urlunsplit(parts)
return safe_next_path

View File

@@ -4,7 +4,7 @@ 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 create_and_login_user, logout_and_redirect_to_index
from redash.authentication import create_and_login_user, logout_and_redirect_to_index, get_next_path
from redash.authentication.org_resolving import current_org
logger = logging.getLogger('google_oauth')
@@ -102,6 +102,7 @@ def authorized():
if user is None:
return logout_and_redirect_to_index()
next_path = request.args.get('state') or url_for("redash.index", org_slug=org.slug)
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 redirect(next_path)

View File

@@ -0,0 +1,65 @@
import logging
import json
import jwt
import requests
logger = logging.getLogger('jwt_auth')
def get_public_keys(url):
"""
Returns:
List of RSA public keys usable by PyJWT.
"""
key_cache = get_public_keys.key_cache
if url in key_cache:
return key_cache[url]
else:
r = requests.get(url)
r.raise_for_status()
data = r.json()
if 'keys' in data:
public_keys = []
for key_dict in data['keys']:
public_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(key_dict))
public_keys.append(public_key)
get_public_keys.key_cache[url] = public_keys
return public_keys
else:
get_public_keys.key_cache[url] = data
return data
get_public_keys.key_cache = {}
def verify_jwt_token(jwt_token, expected_issuer, expected_audience, algorithms, public_certs_url):
# https://developers.cloudflare.com/access/setting-up-access/validate-jwt-tokens/
# https://cloud.google.com/iap/docs/signed-headers-howto
# Loop through the keys since we can't pass the key set to the decoder
keys = get_public_keys(public_certs_url)
key_id = jwt.get_unverified_header(jwt_token).get('kid', '')
if key_id and isinstance(keys, dict):
keys = [keys.get(key_id)]
valid_token = False
payload = None
for key in keys:
try:
# decode returns the claims which has the email if you need it
payload = jwt.decode(
jwt_token,
key=key,
audience=expected_audience,
algorithms=algorithms
)
issuer = payload['iss']
if issuer != expected_issuer:
raise Exception('Wrong issuer: {}'.format(issuer))
valid_token = True
break
except Exception as e:
logging.exception(e)
return payload, valid_token

View File

@@ -13,7 +13,7 @@ except ImportError:
logger.error("The ldap3 library was not found. This is required to use LDAP authentication (see requirements.txt).")
exit()
from redash.authentication import create_and_login_user, logout_and_redirect_to_index
from redash.authentication import create_and_login_user, logout_and_redirect_to_index, get_next_path
from redash.authentication.org_resolving import current_org
@@ -23,7 +23,8 @@ blueprint = Blueprint('ldap_auth', __name__)
@blueprint.route("/ldap/login", methods=['GET', 'POST'])
def login(org_slug=None):
index_url = url_for("redash.index", org_slug=org_slug)
next_path = request.args.get('next', index_url)
unsafe_next_path = request.args.get('next', index_url)
next_path = get_next_path(unsafe_next_path)
if not settings.LDAP_LOGIN_ENABLED:
logger.error("Cannot use LDAP for login without being enabled in settings")

View File

@@ -1,6 +1,6 @@
import logging
from flask import redirect, url_for, Blueprint, request
from redash.authentication import create_and_login_user, logout_and_redirect_to_index
from redash.authentication import create_and_login_user, logout_and_redirect_to_index, get_next_path
from redash.authentication.org_resolving import current_org
from redash.handlers.base import org_scoped_rule
from redash import settings
@@ -11,7 +11,8 @@ blueprint = Blueprint('remote_user_auth', __name__)
@blueprint.route(org_scoped_rule("/remote_user/login"))
def login(org_slug=None):
next_path = request.args.get('next')
unsafe_next_path = request.args.get('next')
next_path = get_next_path(unsafe_next_path)
if not settings.REMOTE_USER_LOGIN_ENABLED:
logger.error("Cannot use remote user for login without being enabled in settings")

View File

@@ -4,7 +4,7 @@ from flask import abort, flash, redirect, render_template, request, url_for
from flask_login import current_user, login_required, login_user, logout_user
from redash import __version__, limiter, models, settings
from redash.authentication import current_org, get_login_url
from redash.authentication import current_org, get_login_url, get_next_path
from redash.authentication.account import (BadSignature, SignatureExpired,
send_password_reset_email,
validate_token)
@@ -106,8 +106,9 @@ def login(org_slug=None):
elif current_org == None:
return redirect('/')
index_url = url_for("redash.index", org_slug=org_slug)
next_path = request.args.get('next', index_url)
index_url = url_for('redash.index', org_slug=org_slug)
unsafe_next_path = request.args.get('next', index_url)
next_path = get_next_path(unsafe_next_path)
if current_user.is_authenticated:
return redirect(next_path)

View File

@@ -17,11 +17,26 @@ SAML_LOGIN_ENABLED = SAML_METADATA_URL != ""
DATE_FORMAT = os.environ.get("REDASH_DATE_FORMAT", "DD/MM/YY")
JWT_LOGIN_ENABLED = parse_boolean(os.environ.get("REDASH_JWT_LOGIN_ENABLED", "false"))
JWT_AUTH_ISSUER = os.environ.get("REDASH_JWT_AUTH_ISSUER", "")
JWT_AUTH_PUBLIC_CERTS_URL = os.environ.get("REDASH_JWT_AUTH_PUBLIC_CERTS_URL", "")
JWT_AUTH_AUDIENCE = os.environ.get("REDASH_JWT_AUTH_AUDIENCE", "")
JWT_AUTH_ALGORITHMS = os.environ.get("REDASH_JWT_AUTH_ALGORITHMS", "HS256,RS256,ES256").split(',')
JWT_AUTH_COOKIE_NAME = os.environ.get("REDASH_JWT_AUTH_COOKIE_NAME", "")
JWT_AUTH_HEADER_NAME = os.environ.get("REDASH_JWT_AUTH_HEADER_NAME", "")
settings = {
"auth_password_login_enabled": PASSWORD_LOGIN_ENABLED,
"auth_saml_enabled": SAML_LOGIN_ENABLED,
"auth_saml_entity_id": SAML_ENTITY_ID,
"auth_saml_metadata_url": SAML_METADATA_URL,
"auth_saml_nameid_format": SAML_NAMEID_FORMAT,
"date_format": DATE_FORMAT
"date_format": DATE_FORMAT,
"auth_jwt_login_enabled": JWT_LOGIN_ENABLED,
"auth_jwt_auth_issuer": JWT_AUTH_ISSUER,
"auth_jwt_auth_public_certs_url": JWT_AUTH_PUBLIC_CERTS_URL,
"auth_jwt_auth_audience": JWT_AUTH_AUDIENCE,
"auth_jwt_auth_algorithms": JWT_AUTH_ALGORITHMS,
"auth_jwt_auth_cookie_name": JWT_AUTH_COOKIE_NAME,
"auth_jwt_auth_header_name": JWT_AUTH_HEADER_NAME,
}

View File

@@ -33,7 +33,7 @@ wsgiref==0.1.2
honcho==0.5.0
statsd==2.1.2
gunicorn==19.7.1
celery==3.1.25
celery==3.1.26.post2
jsonschema==2.4.0
RestrictedPython==3.6.0
pysaml2==4.5.0
@@ -44,6 +44,7 @@ semver==2.2.1
xlsxwriter==0.9.3
pystache==0.5.4
parsedatetime==2.1
PyJWT==1.6.4
cryptography==2.0.2
simplejson==3.10.0
ua-parser==0.7.3

View File

@@ -1,3 +1,4 @@
from passlib.apps import custom_app_context as pwd_context
import redash.models
from redash.models import db
from redash.permissions import ACCESS_TYPE_MODIFY
@@ -41,6 +42,7 @@ class Sequence(object):
user_factory = ModelFactory(redash.models.User,
name='John Doe', email=Sequence(u'test{}@example.com'),
password_hash=pwd_context.encrypt('test1234'),
group_ids=[2],
org_id=1)

View File

@@ -184,6 +184,34 @@ class TestGetLoginUrl(BaseTestCase):
with self.app.test_request_context('/{}_notexists/'.format(self.factory.org.slug)):
self.assertEqual(get_login_url(next=None), '/')
class TestRedirectToUrlAfterLoggingIn(BaseTestCase):
def setUp(self):
super(TestRedirectToUrlAfterLoggingIn, self).setUp()
self.user = self.factory.user
self.password = 'test1234'
def test_no_next_param(self):
response = self.post_request('/login', data={'email': self.user.email, 'password': self.password}, org=self.factory.org)
self.assertEqual(response.location, 'http://localhost/{}/'.format(self.user.org.slug))
def test_simple_path_in_next_param(self):
response = self.post_request('/login?next=queries', data={'email': self.user.email, 'password': self.password}, org=self.factory.org)
self.assertEqual(response.location, 'http://localhost/queries')
def test_starts_scheme_url_in_next_param(self):
response = self.post_request('/login?next=https://redash.io', data={'email': self.user.email, 'password': self.password}, org=self.factory.org)
self.assertEqual(response.location, 'http://localhost/')
def test_without_scheme_url_in_next_param(self):
response = self.post_request('/login?next=//redash.io', data={'email': self.user.email, 'password': self.password}, org=self.factory.org)
self.assertEqual(response.location, 'http://localhost/')
def test_without_scheme_with_path_url_in_next_param(self):
response = self.post_request('/login?next=//localhost/queries', data={'email': self.user.email, 'password': self.password}, org=self.factory.org)
self.assertEqual(response.location, 'http://localhost/queries')
class TestRemoteUserAuth(BaseTestCase):
DEFAULT_SETTING_OVERRIDES = {
'REDASH_REMOTE_USER_LOGIN_ENABLED': 'true'