You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@superset.apache.org by GitBox <gi...@apache.org> on 2018/02/09 05:15:01 UTC

[GitHub] thakur00mayank closed pull request #3729: Added multi-tenancy support.

thakur00mayank closed pull request #3729: Added multi-tenancy support.
URL: https://github.com/apache/incubator-superset/pull/3729
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/.gitignore b/.gitignore
index df190c8abe..9039cdd886 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,3 +37,4 @@ superset/assets/version_info.json
 
 # IntelliJ
 *.iml
+venv
diff --git a/docs/installation.rst b/docs/installation.rst
index 8cd253b15b..8f0734bbd9 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -196,6 +196,7 @@ of the parameters you can copy / paste in that configuration module: ::
     SUPERSET_WORKERS = 4
 
     SUPERSET_WEBSERVER_PORT = 8088
+    ENABLE_MULTI_TENANCY = False
     #---------------------------------------------------------
 
     #---------------------------------------------------------
@@ -235,6 +236,16 @@ auth postback endpoint, you can add them to *WTF_CSRF_EXEMPT_LIST*
 
      WTF_CSRF_EXEMPT_LIST = ['']
 
+Enable Multi Tenancy
+---------------------
+
+To achieve multi-tenancy follow following steps:
+
+* set *ENABLE_MULTI_TENANCY = True* in superset_config file.
+* add column *tenant_id StringDataType(256)* in the tables or views in which you want multi-tenancy. This tenant_id is the same tenant_id as in ab_user table.
+* Make sure that ab_user table have the column *tenant_id* else alter the table to add column tenant_id.
+* if you want to enable multi-tenancy with *CUSTOM_SECURITY_MANAGER*, then your custom security manager class should be a subclass of *MultiTenantSecurityManager* class.
+
 Database dependencies
 ---------------------
 
diff --git a/superset/__init__.py b/superset/__init__.py
index 1e563031df..799e76a551 100644
--- a/superset/__init__.py
+++ b/superset/__init__.py
@@ -19,6 +19,7 @@
 
 from superset.connectors.connector_registry import ConnectorRegistry
 from superset import utils, config  # noqa
+from superset.multi_tenant import MultiTenantSecurityManager
 
 APP_DIR = os.path.dirname(__file__)
 CONFIG_MODULE = os.environ.get('SUPERSET_CONFIG', 'superset.config')
@@ -144,13 +145,22 @@ class MyIndexView(IndexView):
     def index(self):
         return redirect('/superset/welcome')
 
+security_manager_classs = app.config.get("CUSTOM_SECURITY_MANAGER")
+if app.config.get("ENABLE_MULTI_TENANCY"):
+    if security_manager_classs is not None and \
+        not issubclass(security_manager_classs, MultiTenantSecurityManager):
+        print("Not using the configured CUSTOM_SECURITY_MANAGER \
+            as ENABLE_MULTI_TENANCY is True and CUSTOM_SECURITY_MANAGER \
+            is not subclass of MultiTenantSecurityManager.")
+    print("Using MultiTenantSecurityManager as AppBuilder security_manager_class.")
+    security_manager_classs = MultiTenantSecurityManager
 
 appbuilder = AppBuilder(
     app,
     db.session,
     base_template='superset/base.html',
     indexview=MyIndexView,
-    security_manager_class=app.config.get("CUSTOM_SECURITY_MANAGER"))
+    security_manager_class=security_manager_classs)
 
 sm = appbuilder.sm
 
diff --git a/superset/config.py b/superset/config.py
index 4a12bff149..96a69b996c 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -48,6 +48,9 @@
 SUPERSET_WEBSERVER_PORT = 8088
 SUPERSET_WEBSERVER_TIMEOUT = 60
 EMAIL_NOTIFICATIONS = False
+ENABLE_MULTI_TENANCY = False
+# CUSTOM_SECURITY_MANAGER will not be used if ENABLE_MULTI_TENANCY
+# is True and it is not a subclass of MultiTenantSecurityManager class.
 CUSTOM_SECURITY_MANAGER = None
 SQLALCHEMY_TRACK_MODIFICATIONS = False
 # ---------------------------------------------------------
diff --git a/superset/multi_tenant.py b/superset/multi_tenant.py
new file mode 100644
index 0000000000..9d9dce44a0
--- /dev/null
+++ b/superset/multi_tenant.py
@@ -0,0 +1,37 @@
+from flask_appbuilder.security.sqla.manager import SecurityManager
+from flask_appbuilder.security.sqla.models import User
+from sqlalchemy import Column, Integer, ForeignKey, String, Sequence, Table
+from sqlalchemy.orm import relationship, backref
+from flask_appbuilder import Model
+from flask_appbuilder.security.views import UserDBModelView
+from flask_babel import lazy_gettext
+
+class MultiTenantUser(User):
+    tenant_id = Column(String(256))
+
+class MultiTenantUserDBModelView(UserDBModelView):
+    show_fieldsets = [
+        (lazy_gettext('User info'),
+         {'fields': ['username', 'active', 'roles', 'login_count', 'tenant_id']}),
+        (lazy_gettext('Personal Info'),
+         {'fields': ['first_name', 'last_name', 'email'], 'expanded': True}),
+        (lazy_gettext('Audit Info'),
+         {'fields': ['last_login', 'fail_login_count', 'created_on',
+                     'created_by', 'changed_on', 'changed_by'], 'expanded': False}),
+    ]
+
+    user_show_fieldsets = [
+        (lazy_gettext('User info'),
+         {'fields': ['username', 'active', 'roles', 'login_count']}),
+        (lazy_gettext('Personal Info'),
+         {'fields': ['first_name', 'last_name', 'email'], 'expanded': True}),
+    ]
+
+    add_columns = ['first_name', 'last_name', 'username', 'active', 'email', 'roles', 'tenant_id', 'password', 'conf_password']
+    list_columns = ['first_name', 'last_name', 'username', 'email', 'active', 'roles']
+    edit_columns = ['first_name', 'last_name', 'username', 'active', 'email', 'roles', 'tenant_id']
+
+# This will add multi tenant support in user model
+class MultiTenantSecurityManager(SecurityManager):
+    user_model = MultiTenantUser
+    userdbmodelview = MultiTenantUserDBModelView
\ No newline at end of file
diff --git a/superset/security.py b/superset/security.py
index 11b6b647c1..ebcda3b1e2 100644
--- a/superset/security.py
+++ b/superset/security.py
@@ -35,7 +35,8 @@
     'ResetPasswordView',
     'RoleModelView',
     'Security',
-    'UserDBModelView',
+    'UserDBModelView' if not conf.get('ENABLE_MULTI_TENANCY')\
+        else 'MultiTenantUserDBModelView',
 }
 
 ADMIN_ONLY_PERMISSIONS = {
diff --git a/superset/views/core.py b/superset/views/core.py
index bd4d4e52ac..01d554b195 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -701,7 +701,13 @@ def update_role(self):
 
         role_name = data['role_name']
         role = sm.find_role(role_name)
-        role.user = existing_users
+        # This will fetch the User objects instead of sm.user_model as role.user
+        # expect the User object.
+        role_users = []
+        for user in existing_users:
+            role_users.append(db.session.query(ab_models.User).filter(
+                ab_models.User.username == user.username).first())
+        role.user = role_users
         sm.get_session.commit()
         return self.json_response({
             'role': role_name,
diff --git a/superset/viz.py b/superset/viz.py
index 025e9c52b0..f89d2cc6ee 100644
--- a/superset/viz.py
+++ b/superset/viz.py
@@ -22,7 +22,7 @@
 
 import pandas as pd
 import numpy as np
-from flask import request
+from flask import request, g
 from flask_babel import lazy_gettext as _
 from markdown import markdown
 import simplejson as json
@@ -125,6 +125,23 @@ def get_df(self, query_obj=None):
             df = df.fillna(fillna)
         return df
 
+    def append_tenant_filter(self, extras):
+        try:
+            current_user = g.user
+        except Exception as e:
+            return extras
+        if not (current_user and current_user.is_authenticated()):
+            return extras
+        # Add custom filter for non admin role only.
+        if not any([r.name in ['Admin'] for r in current_user.roles]):
+            # Fetch the custom filter from ab_user table
+            if self.datasource.get_col('tenant_id') is not None:
+                tenant_id = current_user.tenant_id or ''
+                multi_tenant_filter = "tenant_id='{}'".format(tenant_id)
+                extras['where'] = (multi_tenant_filter if extras['where'] == '' \
+                    else extras['where'] + ' AND ' + multi_tenant_filter)
+        return extras
+
     def query_obj(self):
         """Building a query object"""
         form_data = self.form_data
@@ -185,6 +202,9 @@ def query_obj(self):
             'druid_time_origin': form_data.get("druid_time_origin", ''),
         }
         filters = form_data.get('filters', [])
+        # Added custom filter to support multi-tenanacy
+        if config.get('ENABLE_MULTI_TENANCY'):
+            extras = self.append_tenant_filter(extras)
         d = {
             'granularity': granularity,
             'from_dttm': from_dttm,
diff --git a/tests/access_tests.py b/tests/access_tests.py
index ec1ce7483d..6d06eba22c 100644
--- a/tests/access_tests.py
+++ b/tests/access_tests.py
@@ -562,8 +562,12 @@ def test_update_role(self):
             follow_redirects=True
         )
         update_role = sm.find_role(update_role_str)
+        update_role_users = []
+        # Convert the User model to sm.user_model
+        for user in update_role.user:
+            update_role_users.append(sm.find_user(username=user.username))
         self.assertEquals(
-            update_role.user, [sm.find_user(username='gamma')])
+            update_role_users, [sm.find_user(username='gamma')])
         self.assertEquals(resp.status_code, 201)
 
         resp = self.client.post(
@@ -586,8 +590,12 @@ def test_update_role(self):
         )
         self.assertEquals(resp.status_code, 201)
         update_role = sm.find_role(update_role_str)
+        update_role_users = []
+        # Convert the User model to sm.user_model
+        for user in update_role.user:
+            update_role_users.append(sm.find_user(username=user.username))
         self.assertEquals(
-            update_role.user, [
+            update_role_users, [
                 sm.find_user(username='alpha'),
                 sm.find_user(username='unknown'),
             ])
diff --git a/tests/core_tests.py b/tests/core_tests.py
index c5335538db..ff28b65640 100644
--- a/tests/core_tests.py
+++ b/tests/core_tests.py
@@ -119,7 +119,7 @@ def assert_admin_view_menus_in(role_name, assert_func):
             assert_func('ResetPasswordView', view_menus)
             assert_func('RoleModelView', view_menus)
             assert_func('Security', view_menus)
-            assert_func('UserDBModelView', view_menus)
+            assert_func(sm.userdbmodelview.__name__, view_menus)
             assert_func('SQL Lab',
                         view_menus)
             assert_func('AccessRequestsModelView', view_menus)
diff --git a/tests/security_tests.py b/tests/security_tests.py
index c107024dc0..1063e24b0b 100644
--- a/tests/security_tests.py
+++ b/tests/security_tests.py
@@ -80,7 +80,7 @@ def assert_cannot_alpha(self, perm_set):
         self.assert_cannot_write('AccessRequestsModelView', perm_set)
         self.assert_cannot_write('Queries', perm_set)
         self.assert_cannot_write('RoleModelView', perm_set)
-        self.assert_cannot_write('UserDBModelView', perm_set)
+        self.assert_cannot_write(sm.userdbmodelview.__name__, perm_set)
 
     def assert_can_admin(self, perm_set):
         self.assert_can_all('DatabaseAsync', perm_set)
@@ -88,7 +88,7 @@ def assert_can_admin(self, perm_set):
         self.assert_can_all('DruidClusterModelView', perm_set)
         self.assert_can_all('AccessRequestsModelView', perm_set)
         self.assert_can_all('RoleModelView', perm_set)
-        self.assert_can_all('UserDBModelView', perm_set)
+        self.assert_can_all(sm.userdbmodelview.__name__, perm_set)
 
         self.assertIn(('all_database_access', 'all_database_access'), perm_set)
         self.assertIn(('can_override_role_permissions', 'Superset'), perm_set)
@@ -111,7 +111,7 @@ def test_is_admin_only(self):
                 'can_show', 'AccessRequestsModelView')))
         self.assertTrue(security.is_admin_only(
             sm.find_permission_view_menu(
-                'can_edit', 'UserDBModelView')))
+                'can_edit', sm.userdbmodelview.__name__)))
         self.assertTrue(security.is_admin_only(
             sm.find_permission_view_menu(
                 'can_approve', 'Superset')))


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services