Merge pull request #1060 from thoughtworks/saml-authorization

Feature: support configuring user's groups with SAML
This commit is contained in:
Arik Fraimovich 2016-05-26 23:07:30 +03:00
commit 214aa3b799
5 changed files with 89 additions and 2 deletions

34
docs/dev/saml.rst Normal file
View File

@ -0,0 +1,34 @@
SAML Authentication and Authorization
#####################################
Authentication
==============
Add to your .env file REDASH_SAML_METADATA_URL config value which
needs to point to the SAML provider metadata url, eg https://app.onelogin.com/saml/metadata/
And an optional REDASH_SAML_CALLBACK_SERVER_NAME which contains the
server name of the redash server for the callbacks from the SAML provider (eg demo.redash.io)
On the SAML provider side, example configuration for OneLogin is:
SAML Consumer URL: http://demo.redash.io/saml/login
SAML Audience: http://demo.redash.io/saml/callback
SAML Recipient: http://demo.redash.io/saml/callback
Example configuration for Okta is:
Single Sign On URL: http://demo.redash.io/saml/callback
Recipient URL: http://demo.redash.io/saml/callback
Destination URL: http://demo.redash.io/saml/callback
with parameters 'FirstName' and 'LastName', both configured to be included in the SAML assertion.
Authorization
=============
To manage group assignments in Redash using your SAML provider, configure SAML response to include
attribute with key 'RedashGroups', and value as names of groups in Redash.
Example configuration for Okta is:
In the Group Attribute Statements -
Name: RedashGroups
Filter: Starts with: this-is-a-group-in-redash

View File

@ -69,6 +69,8 @@ def create_and_login_user(org, name, email):
login_user(user_object, remember=True)
return user_object
@blueprint.route('/<org_slug>/oauth/google', endpoint="authorize_org")
def org_login(org_slug):

View File

@ -85,7 +85,12 @@ def idp_initiated():
# This is what as known as "Just In Time (JIT) provisioning".
# What that means is that, if a user in a SAML assertion
# isn't in the user store, we create that user first, then log them in
create_and_login_user(current_org, name, email)
user = create_and_login_user(current_org, name, email)
if 'RedashGroups' in authn_response.ava:
group_names = authn_response.ava.get('RedashGroups')
user.update_group_assignments(group_names)
url = url_for('redash.index')
return redirect(url)

View File

@ -244,6 +244,11 @@ class Group(BaseModel, BelongsToOrgMixin):
def members(cls, group_id):
return User.select().where(peewee.SQL("%s = ANY(groups)", group_id))
@classmethod
def find_by_name(cls, org, group_names):
result = cls.select().where(cls.org == org, cls.name << group_names)
return list(result)
def __unicode__(self):
return unicode(self.id)
@ -330,6 +335,12 @@ class User(ModelTimestampsMixin, BaseModel, BelongsToOrgMixin, UserMixin, Permis
def verify_password(self, password):
return self.password_hash and pwd_context.verify(password, self.password_hash)
def update_group_assignments(self, group_names):
groups = Group.find_by_name(self.org, group_names)
groups.append(self.org.default_group)
self.groups = map(lambda g: g.id, groups)
self.save()
class ConfigurationField(peewee.TextField):
def db_value(self, value):

View File

@ -276,7 +276,6 @@ class QueryArchiveTest(BaseTestCase):
self.assertEqual(None, query.schedule)
class DataSourceTest(BaseTestCase):
def test_get_schema(self):
return_value = [{'name': 'table', 'columns': []}]
@ -415,6 +414,42 @@ class TestQueryAll(BaseTestCase):
self.assertIn(q2, models.Query.all_queries([group1, group2]))
class TestUser(BaseTestCase):
def test_default_group_always_added(self):
user = self.factory.create_user()
user.update_group_assignments(["g_unknown"])
self.assertItemsEqual([user.org.default_group.id], user.groups)
def test_update_group_assignments(self):
user = self.factory.user
new_group = models.Group.create(id='999', name="g1", org=user.org)
user.update_group_assignments(["g1"])
self.assertItemsEqual([user.org.default_group.id, new_group.id], user.groups)
class TestGroup(BaseTestCase):
def test_returns_groups_with_specified_names(self):
org1 = self.factory.create_org()
org2 = self.factory.create_org()
matching_group1 = models.Group.create(id='999', name="g1", org=org1)
matching_group2 = models.Group.create(id='888', name="g2", org=org1)
non_matching_group = models.Group.create(id='777', name="g1", org=org2)
groups = models.Group.find_by_name(org1, ["g1", "g2"])
self.assertIn(matching_group1, groups)
self.assertIn(matching_group2, groups)
self.assertNotIn(non_matching_group, groups)
def test_returns_no_groups(self):
org1 = self.factory.create_org()
models.Group.create(id='999', name="g1", org=org1)
self.assertEqual([], models.Group.find_by_name(org1, ["non-existing"]))
class TestQueryResultStoreResult(BaseTestCase):
def setUp(self):
super(TestQueryResultStoreResult, self).setUp()