mirror of
https://github.com/valitydev/redash.git
synced 2024-11-07 01:25:16 +00:00
Merge pull request #1060 from thoughtworks/saml-authorization
Feature: support configuring user's groups with SAML
This commit is contained in:
commit
214aa3b799
34
docs/dev/saml.rst
Normal file
34
docs/dev/saml.rst
Normal 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
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user