Skip to content

Commit

Permalink
Merge pull request #98 from EverythingMe/feature_allow_external_users
Browse files Browse the repository at this point in the history
Feature: allow external users
  • Loading branch information
arikfr committed Feb 13, 2014
2 parents fc0b118 + 8ad2c2a commit 4c39047
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 28 deletions.
66 changes: 43 additions & 23 deletions redash/authentication.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import functools
import hashlib
import hmac
from flask import request, make_response
from flask.ext.googleauth import GoogleFederated
from flask import current_app, request, make_response, g, redirect, url_for
from flask.ext.googleauth import GoogleAuth
import time
from werkzeug.contrib.fixers import ProxyFix
import werkzeug.wrappers
from redash import models, settings


Expand All @@ -23,36 +22,57 @@ class HMACAuthentication(object):
def __init__(self, auth):
self.auth = auth

def required(self, fn):
wrapped_fn = self.auth.required(fn)
@staticmethod
def api_key_authentication():
signature = request.args.get('signature')
expires = float(request.args.get('expires') or 0)
query_id = request.view_args.get('query_id', None)

@functools.wraps(fn)
def decorated(*args, **kwargs):
signature = request.args.get('signature')
expires = float(request.args.get('expires') or 0)
query_id = request.view_args.get('query_id', None)
# TODO: 3600 should be a setting
if signature and query_id and time.time() < expires <= time.time() + 3600:
query = models.Query.get(models.Query.id == query_id)
calculated_signature = sign(query.api_key, request.path, expires)

if query.api_key and signature == calculated_signature:
return True

return False

# TODO: 3600 should be a setting
if signature and query_id and time.time() < expires <= time.time() + 3600:
query = models.Query.get(models.Query.id == query_id)
calculated_signature = sign(query.api_key, request.path, expires)
@staticmethod
def is_user_logged_in():
return g.user is not None

if query.api_key and signature == calculated_signature:
return fn(*args, **kwargs)
@staticmethod
def valid_user():
email = g.user['email']
if not settings.GOOGLE_APPS_DOMAIN:
return True

return email in settings.ALLOWED_EXTERNAL_USERS or email.endswith("@%s" % settings.GOOGLE_APPS_DOMAIN)

def required(self, fn):
@functools.wraps(fn)
def decorated(*args, **kwargs):
if self.is_user_logged_in() and self.valid_user():
return fn(*args, **kwargs)

# Work around for flask-restful testing only for flask.wrappers.Resource instead of
# werkzeug.wrappers.Response
resp = wrapped_fn(*args, **kwargs)
if isinstance(resp, werkzeug.wrappers.Response):
resp = make_response(resp)
if self.api_key_authentication():
return fn(*args, **kwargs)

return resp
blueprint = current_app.extensions['googleauth'].blueprint
# The make_response call is a work around for flask-restful testing only for
# flask.wrappers.Resource instead of werkzeug.wrappers.Response
return make_response(redirect(url_for("%s.login" % blueprint.name, next=request.url)))

return decorated


def setup_authentication(app):
openid_auth = GoogleFederated(settings.GOOGLE_APPS_DOMAIN, app)
openid_auth = GoogleAuth(app)
# If we don't have a list of external users, we can use Google's federated login, which limits
# the domain with which you can sign in.
if not settings.ALLOWED_EXTERNAL_USERS and settings.GOOGLE_APPS_DOMAIN:
openid_auth._OPENID_ENDPOINT = "https://www.google.com/a/%s/o8/ud?be=o8" % settings.GOOGLE_APPS_DOMAIN
app.wsgi_app = ProxyFix(app.wsgi_app)
app.secret_key = settings.COOKIE_SECRET

Expand Down
11 changes: 10 additions & 1 deletion redash/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ def fix_assets_path(path):
fullpath = os.path.join(os.path.dirname(__file__), path)
return fullpath


def array_from_string(str):
array = str.split(',')
if "" in array:
array.remove("")

return array

REDIS_URL = os.environ.get('REDASH_REDIS_URL', "redis://localhost:6379")

# "pg", "graphite" or "mysql"
Expand All @@ -41,7 +49,8 @@ def fix_assets_path(path):
# access
GOOGLE_APPS_DOMAIN = os.environ.get("REDASH_GOOGLE_APPS_DOMAIN", "")
# Email addresses of admin users (comma separated)
ADMINS = os.environ.get("REDASH_ADMINS", '').split(',')
ADMINS = array_from_string(os.environ.get("REDASH_ADMINS", ''))
ALLOWED_EXTERNAL_USERS = array_from_string(os.environ.get("REDASH_ALLOWED_EXTERNAL_USERS", ''))
STATIC_ASSETS_PATH = fix_assets_path(os.environ.get("REDASH_STATIC_ASSETS_PATH", "../rd_ui/dist/"))
WORKERS_COUNT = int(os.environ.get("REDASH_WORKERS_COUNT", "2"))
COOKIE_SECRET = os.environ.get("REDASH_COOKIE_SECRET", "c292a0a3aa32397cdb050e233733900f")
Expand Down
46 changes: 42 additions & 4 deletions tests/test_controllers.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
from contextlib import contextmanager
import json
import time
from unittest import TestCase
from tests import BaseTestCase
from tests.factories import dashboard_factory, widget_factory, visualization_factory, query_factory, \
query_result_factory
from redash import app, models
from redash import app, models, settings
from redash.utils import json_dumps
from redash.authentication import sign


settings.GOOGLE_APPS_DOMAIN = "example.com"

@contextmanager
def authenticated_user(c, user='test@example.com', name='John Test'):
with c.session_transaction() as sess:
Expand Down Expand Up @@ -45,7 +48,42 @@ def test_returns_content_when_authenticated(self):
self.assertEquals(200, rv.status_code)


class PingTest(BaseTestCase):
class TestAuthentication(TestCase):
def test_redirects_for_nonsigned_in_user(self):
with app.test_client() as c:
rv = c.get("/")
self.assertEquals(302, rv.status_code)

def test_returns_content_when_authenticated_with_correct_domain(self):
settings.GOOGLE_APPS_DOMAIN = "example.com"
with app.test_client() as c, authenticated_user(c, user="test@example.com"):
rv = c.get("/")
self.assertEquals(200, rv.status_code)

def test_redirects_when_authenticated_with_wrong_domain(self):
settings.GOOGLE_APPS_DOMAIN = "example.com"
with app.test_client() as c, authenticated_user(c, user="test@not-example.com"):
rv = c.get("/")
self.assertEquals(302, rv.status_code)

def test_returns_content_when_user_in_allowed_list(self):
settings.GOOGLE_APPS_DOMAIN = "example.com"
settings.ALLOWED_EXTERNAL_USERS = ["test@not-example.com"]

with app.test_client() as c, authenticated_user(c, user="test@not-example.com"):
rv = c.get("/")
self.assertEquals(200, rv.status_code)

def test_returns_content_when_google_apps_domain_empty(self):
settings.GOOGLE_APPS_DOMAIN = ""
settings.ALLOWED_EXTERNAL_USERS = []

with app.test_client() as c, authenticated_user(c, user="test@whatever.com"):
rv = c.get("/")
self.assertEquals(200, rv.status_code)


class PingTest(TestCase):
def test_ping(self):
with app.test_client() as c:
rv = c.get('/ping')
Expand Down Expand Up @@ -83,7 +121,7 @@ def test_get_non_existint_dashbaord(self):
self.assertEquals(rv.status_code, 404)

def test_create_new_dashboard(self):
user_email = 'test@everything.me'
user_email = 'test@example.com'
with app.test_client() as c, authenticated_user(c, user=user_email):
dashboard_name = 'Test Dashboard'
rv = json_request(c.post, '/api/dashboards', data={'name': dashboard_name})
Expand Down Expand Up @@ -182,7 +220,7 @@ def test_update_query(self):
self.assertEquals(rv.json['name'], 'Testing')

def test_create_query(self):
user = 'test@everything.me'
user = 'test@example.com'
query_data = {
'name': 'Testing',
'query': 'SELECT 1',
Expand Down

0 comments on commit 4c39047

Please sign in to comment.