diff --git a/caravel/migrations/versions/5e4a03ef0bf0_add_request_access_model.py b/caravel/migrations/versions/5e4a03ef0bf0_add_request_access_model.py
new file mode 100644
index 0000000000000..ad6375f1837c4
--- /dev/null
+++ b/caravel/migrations/versions/5e4a03ef0bf0_add_request_access_model.py
@@ -0,0 +1,33 @@
+"""Add access_request table to manage requests to access datastores.
+
+Revision ID: 5e4a03ef0bf0
+Revises: 41f6a59a61f2
+Create Date: 2016-09-09 17:39:57.846309
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+# revision identifiers, used by Alembic.
+revision = '5e4a03ef0bf0'
+down_revision = 'b347b202819b'
+
+
+def upgrade():
+ op.create_table(
+ 'access_request',
+ sa.Column('created_on', sa.DateTime(), nullable=True),
+ sa.Column('changed_on', sa.DateTime(), nullable=True),
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('datasource_type', sa.String(length=200), nullable=True),
+ sa.Column('datasource_id', sa.Integer(), nullable=True),
+ sa.Column('changed_by_fk', sa.Integer(), nullable=True),
+ sa.Column('created_by_fk', sa.Integer(), nullable=True),
+ sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
+ sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+
+
+def downgrade():
+ op.drop_table('access_request')
diff --git a/caravel/models.py b/caravel/models.py
index c59b49a95514e..38a457b98d4d4 100644
--- a/caravel/models.py
+++ b/caravel/models.py
@@ -26,6 +26,7 @@
from flask_appbuilder import Model
from flask_appbuilder.models.mixins import AuditMixin
from flask_appbuilder.models.decorators import renders
+from flask_appbuilder.security.sqla.models import Role, PermissionView
from flask_babel import lazy_gettext as _
from pydruid.client import PyDruid
@@ -702,6 +703,10 @@ def perm(self):
"[{obj.database}].[{obj.table_name}]"
"(id:{obj.id})").format(obj=self)
+ @property
+ def name(self):
+ return self.table_name
+
@property
def full_name(self):
return "[{obj.database}].[{obj.table_name}]".format(obj=self)
@@ -1202,6 +1207,7 @@ def refresh_datasources(self):
for datasource in self.get_datasources():
if datasource not in config.get('DRUID_DATA_SOURCE_BLACKLIST'):
DruidDatasource.sync_to_db(datasource, self)
+
@property
def perm(self):
return "[{obj.cluster_name}].(id:{obj.id})".format(obj=self)
@@ -2000,6 +2006,7 @@ def to_dict(self):
'tempTable': self.tmp_table_name,
'userId': self.user_id,
}
+
@property
def name(self):
ts = datetime.now().isoformat()
@@ -2007,3 +2014,71 @@ def name(self):
tab = self.tab_name.replace(' ', '_').lower() if self.tab_name else 'notab'
tab = re.sub(r'\W+', '', tab)
return "sqllab_{tab}_{ts}".format(**locals())
+
+
+class DatasourceAccessRequest(Model, AuditMixinNullable):
+ """ORM model for the access requests for datasources and dbs."""
+ __tablename__ = 'access_request'
+ id = Column(Integer, primary_key=True)
+
+ datasource_id = Column(Integer)
+ datasource_type = Column(String(200))
+
+ ROLES_BLACKLIST = set(['Admin', 'Alpha', 'Gamma', 'Public'])
+
+ @property
+ def cls_model(self):
+ return src_registry.sources[self.datasource_type]
+
+ @property
+ def username(self):
+ return self.creator()
+
+ @property
+ def datasource(self):
+ return self.get_datasource
+
+ @datasource.getter
+ @utils.memoized
+ def get_datasource(self):
+ ds = db.session.query(self.cls_model).filter_by(
+ id=self.datasource_id).first()
+ return ds
+
+ @property
+ def datasource_link(self):
+ return self.datasource.link
+
+ @property
+ def roles_with_datasource(self):
+ action_list = ''
+ pv = sm.find_permission_view_menu(
+ 'datasource_access', self.datasource.perm)
+ for r in pv.role:
+ if r.name in self.ROLES_BLACKLIST:
+ continue
+ url = (
+ '/caravel/approve?datasource_type={self.datasource_type}&'
+ 'datasource_id={self.datasource_id}&'
+ 'created_by={self.created_by.username}&role_to_grant={r.name}'
+ .format(**locals())
+ )
+ href = 'Grant {} Role'.format(url, r.name)
+ action_list = action_list + '
' + href + ''
+ return ''
+
+ @property
+ def user_roles(self):
+ action_list = ''
+ for r in self.created_by.roles:
+ url = (
+ '/caravel/approve?datasource_type={self.datasource_type}&'
+ 'datasource_id={self.datasource_id}&'
+ 'created_by={self.created_by.username}&role_to_extend={r.name}'
+ .format(**locals())
+ )
+ href = 'Extend {} Role'.format(url, r.name)
+ if r.name in self.ROLES_BLACKLIST:
+ href = "{} Role".format(r.name)
+ action_list = action_list + '' + href + ''
+ return ''
diff --git a/caravel/templates/caravel/request_access.html b/caravel/templates/caravel/request_access.html
new file mode 100644
index 0000000000000..0c075bac23ddc
--- /dev/null
+++ b/caravel/templates/caravel/request_access.html
@@ -0,0 +1,24 @@
+{% extends "caravel/basic.html" %}
+{% block title %}{{ _("No Access!") }}{% endblock %}
+{% block body %}
+ {% include "caravel/flash_wrapper.html" %}
+
+
+ {{ _("You do not have permissions to access the datasource %(name)s.",
+ name=datasource_name)
+ }}
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/caravel/utils.py b/caravel/utils.py
index 9e2c7f8d780e9..c80b94fc813d7 100644
--- a/caravel/utils.py
+++ b/caravel/utils.py
@@ -212,6 +212,32 @@ def process_result_value(self, value, dialect):
def init(caravel):
"""Inits the Caravel application with security roles and such"""
+ ADMIN_ONLY_VIEW_MENUES = set([
+ 'ResetPasswordView',
+ 'RoleModelView',
+ 'Security',
+ 'UserDBModelView',
+ 'SQL Lab alpha',
+ 'AccessRequestsModelView',
+ ])
+
+ ADMIN_ONLY_PERMISSIONS = set([
+ 'can_sync_druid_source',
+ 'can_approve',
+ ])
+
+ ALPHA_ONLY_PERMISSIONS = set([
+ 'all_datasource_access',
+ 'can_add',
+ 'can_download',
+ 'can_delete',
+ 'can_edit',
+ 'can_save',
+ 'datasource_access',
+ 'database_access',
+ 'muldelete',
+ ])
+
db = caravel.db
models = caravel.models
config = caravel.app.config
@@ -223,44 +249,34 @@ def init(caravel):
merge_perm(sm, 'all_datasource_access', 'all_datasource_access')
perms = db.session.query(ab_models.PermissionView).all()
+ # set alpha and admin permissions
for perm in perms:
if (
perm.permission and
perm.permission.name in ('datasource_access', 'database_access')):
continue
- if perm.view_menu and perm.view_menu.name not in (
- 'ResetPasswordView',
- 'RoleModelView',
- 'Security',
- 'UserDBModelView',
- 'SQL Lab'):
+ if (
+ perm.view_menu and
+ perm.view_menu.name not in ADMIN_ONLY_VIEW_MENUES and
+ perm.permission and
+ perm.permission.name not in ADMIN_ONLY_PERMISSIONS):
sm.add_permission_role(alpha, perm)
sm.add_permission_role(admin, perm)
+
gamma = sm.add_role("Gamma")
public_role = sm.find_role("Public")
public_role_like_gamma = \
public_role and config.get('PUBLIC_ROLE_LIKE_GAMMA', False)
+
+ # set gamma permissions
for perm in perms:
if (
- perm.view_menu and perm.view_menu.name not in (
- 'ResetPasswordView',
- 'RoleModelView',
- 'UserDBModelView',
- 'SQL Lab',
- 'Security') and
+ perm.view_menu and
+ perm.view_menu.name not in ADMIN_ONLY_VIEW_MENUES and
perm.permission and
- perm.permission.name not in (
- 'all_datasource_access',
- 'can_add',
- 'can_download',
- 'can_delete',
- 'can_edit',
- 'can_save',
- 'datasource_access',
- 'database_access',
- 'muldelete',
- )):
+ perm.permission.name not in ADMIN_ONLY_PERMISSIONS and
+ perm.permission.name not in ALPHA_ONLY_PERMISSIONS):
sm.add_permission_role(gamma, perm)
if public_role_like_gamma:
sm.add_permission_role(public_role, perm)
diff --git a/caravel/views.py b/caravel/views.py
index 3aa87f7070651..d87c9ba8a6bbe 100755
--- a/caravel/views.py
+++ b/caravel/views.py
@@ -36,6 +36,7 @@
appbuilder, cache, db, models, viz, utils, app,
sm, ascii_art, sql_lab, src_registry
)
+from caravel.models import DatasourceAccessRequest as DAR
config = app.config
log_this = models.Log.log_this
@@ -74,6 +75,9 @@ class ListWidgetWithCheckboxes(ListWidget):
ALL_DATASOURCE_ACCESS_ERR = __(
"This endpoint requires the `all_datasource_access` permission")
DATASOURCE_MISSING_ERR = __("The datasource seems to have been deleted")
+ACCESS_REQUEST_MISSING_ERR = __(
+ "The access requests seem to have been deleted")
+USER_MISSING_ERR = __("The user seems to have been deleted")
def get_database_access_error_msg(database_name):
@@ -81,13 +85,9 @@ def get_database_access_error_msg(database_name):
"`all_datasource_access` permission", name=database_name)
-def get_datasource_access_error_msg(datasource):
- error = ("This endpoint requires the datasource %(name)s, database or "
- "`all_datasource_access` permission")
- if hasattr(datasource, 'table_name'):
- return __(error, name=datasource.table_name)
- else:
- return __(error, name=datasource.datasource_name)
+def get_datasource_access_error_msg(datasource_name):
+ return __("This endpoint requires the datasource %(name)s, database or "
+ "`all_datasource_access` permission", name=datasource_name)
def get_error_msg():
@@ -628,6 +628,31 @@ def post_update(self, table):
icon='fa-table',)
+class AccessRequestsModelView(CaravelModelView, DeleteMixin):
+ datamodel = SQLAInterface(DAR)
+ list_columns = [
+ 'username', 'user_roles', 'datasource_link',
+ 'roles_with_datasource', 'created_on']
+ order_columns = ['username', 'datasource_link']
+ base_order = ('changed_on', 'desc')
+ label_columns = {
+ 'username': _("User"),
+ 'user_roles': _("User Roles"),
+ 'database': _("Database URL"),
+ 'datasource_link': _("Datasource"),
+ 'roles_with_datasource': _("Roles to grant"),
+ 'created_on': _("Created On"),
+ }
+
+appbuilder.add_view(
+ AccessRequestsModelView,
+ "Access requests",
+ label=__("Access requests"),
+ category="Security",
+ category_label=__("Security"),
+ icon='fa-table',)
+
+
appbuilder.add_separator("Sources")
@@ -968,6 +993,122 @@ def msg(self):
class Caravel(BaseCaravelView):
"""The base views for Caravel!"""
+ @log_this
+ @has_access
+ @expose("/request_access_form///"
+ "")
+ def request_access_form(
+ self, datasource_type, datasource_id, datasource_name):
+ request_access_url = (
+ '/caravel/request_access?datasource_type={}&datasource_id={}&'
+ 'datasource_name=datasource_name'.format(
+ datasource_type, datasource_id, datasource_name)
+ )
+ return self.render_template(
+ 'caravel/request_access.html',
+ request_access_url=request_access_url,
+ datasource_name=datasource_name,
+ slicemodelview_link='/slicemodelview/list/')
+
+ @log_this
+ @has_access
+ @expose("/request_access")
+ def request_access(self):
+ datasource_id = request.args.get('datasource_id')
+ datasource_type = request.args.get('datasource_type')
+ datasource_name = request.args.get('datasource_name')
+ session = db.session
+
+ duplicates = (
+ session.query(DAR)
+ .filter(
+ DAR.datasource_id == datasource_id,
+ DAR.datasource_type == datasource_type,
+ DAR.created_by_fk == g.user.id)
+ .all()
+ )
+
+ if duplicates:
+ flash(__(
+ "You have already requested access to the datasource %(name)s",
+ name=datasource_name), "warning")
+ return redirect('/slicemodelview/list/')
+
+ access_request = DAR(datasource_id=datasource_id,
+ datasource_type=datasource_type)
+ db.session.add(access_request)
+ db.session.commit()
+ flash(__("Access to the datasource %(name)s was requested",
+ name=datasource_name), "info")
+ return redirect('/slicemodelview/list/')
+
+ @log_this
+ @has_access
+ @expose("/approve")
+ def approve(self):
+ datasource_type = request.args.get('datasource_type')
+ datasource_id = request.args.get('datasource_id')
+ created_by_username = request.args.get('created_by')
+ role_to_grant = request.args.get('role_to_grant')
+ role_to_extend = request.args.get('role_to_extend')
+
+ session = db.session
+ datasource_class = src_registry.sources[datasource_type]
+ datasource = session.query(datasource_class).filter_by(
+ id=datasource_id).first()
+
+ if not datasource:
+ flash(DATASOURCE_MISSING_ERR, "alert")
+ return json_error_response(DATASOURCE_MISSING_ERR)
+
+ requested_by = sm.find_user(username=created_by_username)
+ if not requested_by:
+ flash(USER_MISSING_ERR, "alert")
+ return json_error_response(USER_MISSING_ERR)
+
+ requests = (
+ session.query(DAR)
+ .filter(
+ DAR.datasource_id == datasource_id,
+ DAR.datasource_type == datasource_type,
+ DAR.created_by_fk == requested_by.id)
+ .all()
+ )
+
+ if not requests:
+ flash(ACCESS_REQUEST_MISSING_ERR, "alert")
+ return json_error_response(ACCESS_REQUEST_MISSING_ERR)
+
+ # check if you can approve
+ if self.all_datasource_access() or g.user.id == datasource.owner_id:
+ # can by done by admin only
+ if role_to_grant:
+ role = sm.find_role(role_to_grant)
+ requested_by.roles.append(role)
+ flash(__(
+ "%(user)s was granted the role %(role)s that gives access "
+ "to the %(datasource)s",
+ user=requested_by.username,
+ role=role_to_grant,
+ datasource=datasource.full_name), "info")
+
+ if role_to_extend:
+ perm_view = sm.find_permission_view_menu(
+ 'datasource_access', datasource.perm)
+ sm.add_permission_role(sm.find_role(role_to_extend), perm_view)
+ flash(__("Role %(r)s was extended to provide the access to"
+ " the datasource %(ds)s",
+ r=role_to_extend, ds=datasource.full_name), "info")
+
+ else:
+ flash(__("You have no permission to approve this request"),
+ "danger")
+ return redirect('/accessrequestsmodelview/list/')
+ for r in requests:
+ session.delete(r)
+ session.commit()
+ return redirect('/accessrequestsmodelview/list/')
+
@has_access
@expose("/explore////")
@expose("/explore///")
@@ -976,12 +1117,7 @@ class Caravel(BaseCaravelView):
def explore(self, datasource_type, datasource_id, slice_id=None):
error_redirect = '/slicemodelview/list/'
datasource_class = src_registry.sources[datasource_type]
-
- datasources = (
- db.session
- .query(datasource_class)
- .all()
- )
+ datasources = db.session.query(datasource_class).all()
datasources = sorted(datasources, key=lambda ds: ds.full_name)
datasource = [ds for ds in datasources if int(datasource_id) == ds.id]
datasource = datasource[0] if datasource else None
@@ -991,8 +1127,10 @@ def explore(self, datasource_type, datasource_id, slice_id=None):
return redirect(error_redirect)
if not self.datasource_access(datasource):
- flash(__(get_datasource_access_error_msg(datasource)), "danger")
- return redirect(error_redirect)
+ flash(
+ __(get_datasource_access_error_msg(datasource.name)), "danger")
+ return redirect('caravel/request_access_form/{}/{}/{}'.format(
+ datasource_type, datasource_id, datasource.name))
request_args_multi_dict = request.args # MultiDict
@@ -1566,7 +1704,7 @@ def select_star(self, database_id, table_name):
# Prevent exposing column fields to users that cannot access DB.
if not self.datasource_access(t.perm):
- flash(get_datasource_access_error_msg(t), 'danger')
+ flash(get_datasource_access_error_msg(t.name), 'danger')
return redirect("/tablemodelview/list/")
fields = ", ".join(
diff --git a/tests/base_tests.py b/tests/base_tests.py
index 4cae61bf12157..dc781f937887f 100644
--- a/tests/base_tests.py
+++ b/tests/base_tests.py
@@ -10,7 +10,7 @@
from flask_appbuilder.security.sqla import models as ab_models
import caravel
-from caravel import app, db, models, utils, appbuilder
+from caravel import app, db, models, utils, appbuilder, sm
os.environ['CARAVEL_CONFIG'] = 'tests.caravel_test_config'
@@ -22,7 +22,7 @@ class CaravelTestCase(unittest.TestCase):
def __init__(self, *args, **kwargs):
super(CaravelTestCase, self).__init__(*args, **kwargs)
self.client = app.test_client()
-
+ self.maxDiff = None
utils.init(caravel)
admin = appbuilder.sm.find_user('admin')
@@ -46,6 +46,27 @@ def __init__(self, *args, **kwargs):
appbuilder.sm.find_role('Alpha'),
password='general')
+ # create druid cluster and druid datasources
+ session = db.session
+ cluster = session.query(models.DruidCluster).filter_by(
+ cluster_name="druid_test").first()
+ if not cluster:
+ cluster = models.DruidCluster(cluster_name="druid_test")
+ session.add(cluster)
+ session.commit()
+
+ druid_datasource1 = models.DruidDatasource(
+ datasource_name='druid_ds_1',
+ cluster_name='druid_test'
+ )
+ session.add(druid_datasource1)
+ druid_datasource2 = models.DruidDatasource(
+ datasource_name='druid_ds_2',
+ cluster_name='druid_test'
+ )
+ session.add(druid_datasource2)
+ session.commit()
+
utils.init(caravel)
def login(self, username='admin', password='general'):
@@ -71,6 +92,14 @@ def get_latest_query(self, sql):
session.close()
return query
+ def get_access_requests(self, username, ds_type, ds_id):
+ return db.session.query(models.DatasourceAccessRequest).filter(
+ models.DatasourceAccessRequest.created_by_fk ==
+ sm.find_user(username=username).id,
+ models.DatasourceAccessRequest.datasource_type == ds_type,
+ models.DatasourceAccessRequest.datasource_id == ds_id
+ ).all()
+
def logout(self):
self.client.get('/logout/', follow_redirects=True)
diff --git a/tests/core_tests.py b/tests/core_tests.py
index 2b24357997016..1297cfb3088ad 100644
--- a/tests/core_tests.py
+++ b/tests/core_tests.py
@@ -17,7 +17,7 @@
from flask_appbuilder.security.sqla import models as ab_models
import caravel
-from caravel import app, db, models, utils, appbuilder, sm
+from caravel import app, db, models, utils, appbuilder, sm, src_registry
from caravel.models import DruidDatasource
from .base_tests import CaravelTestCase
@@ -25,7 +25,6 @@
BASE_DIR = app.config.get("BASE_DIR")
cli = imp.load_source('cli', BASE_DIR + "/bin/caravel")
-
class CoreTests(CaravelTestCase):
def __init__(self, *args, **kwargs):
@@ -45,11 +44,38 @@ def setUpClass(cls):
def setUp(self):
db.session.query(models.Query).delete()
-
+ db.session.query(models.DatasourceAccessRequest).delete()
def tearDown(self):
pass
+ def test_admin_only_permissions(self):
+ def assert_admin_permission_in(role_name, assert_func):
+ role = sm.find_role(role_name)
+ permissions = [p.permission.name for p in role.permissions]
+ assert_func('can_sync_druid_source', permissions)
+ assert_func('can_approve', permissions)
+
+ assert_admin_permission_in('Admin', self.assertIn)
+ assert_admin_permission_in('Alpha', self.assertNotIn)
+ assert_admin_permission_in('Gamma', self.assertNotIn)
+
+ def test_admin_only_menu_views(self):
+ def assert_admin_view_menus_in(role_name, assert_func):
+ role = sm.find_role(role_name)
+ view_menus = [p.view_menu.name for p in role.permissions]
+ assert_func('ResetPasswordView', view_menus)
+ assert_func('RoleModelView', view_menus)
+ assert_func('Security', view_menus)
+ assert_func('UserDBModelView', view_menus)
+ assert_func('SQL Lab alpha',
+ view_menus)
+ assert_func('AccessRequestsModelView', view_menus)
+
+ assert_admin_view_menus_in('Admin', self.assertIn)
+ assert_admin_view_menus_in('Alpha', self.assertNotIn)
+ assert_admin_view_menus_in('Gamma', self.assertNotIn)
+
def test_save_slice(self):
self.login(username='admin')
@@ -161,7 +187,8 @@ def test_shortner(self):
"flt_col_0=source&flt_op_0=in&flt_eq_0=&slice_id=78&slice_name="
"Energy+Sankey&collapsed_fieldsets=&action=&datasource_name="
"energy_usage&datasource_id=1&datasource_type=table&"
- "previous_viz_type=sankey")
+ "previous_viz_type=sankey"
+ )
resp = self.client.post('/r/shortner/', data=data)
assert '/r/' in resp.data.decode('utf-8')
@@ -210,8 +237,225 @@ def test_add_slices(self, username='admin'):
assert new_slice in dash.slices
assert len(set(dash.slices)) == len(dash.slices)
+ def test_approve(self):
+ session = db.session
+ sm.add_role('table_role')
+ self.login('admin')
+
+ def prepare_request(ds_type, ds_name, role):
+ ds_class = src_registry.sources[ds_type]
+ # TODO: generalize datasource names
+ if ds_type == 'table':
+ ds = session.query(ds_class).filter(
+ ds_class.table_name == ds_name).first()
+ else:
+ ds = session.query(ds_class).filter(
+ ds_class.datasource_name == ds_name).first()
+ ds_perm_view = sm.find_permission_view_menu(
+ 'datasource_access', ds.perm)
+ sm.add_permission_role(sm.find_role(role), ds_perm_view)
+ access_request = models.DatasourceAccessRequest(
+ datasource_id=ds.id,
+ datasource_type=ds_type,
+ created_by_fk=sm.find_user(username='gamma').id,
+ )
+ session.add(access_request)
+ session.commit()
+ return access_request
+
+ EXTEND_ROLE_REQUEST = (
+ '/caravel/approve?datasource_type={}&datasource_id={}&'
+ 'created_by={}&role_to_extend={}')
+ GRANT_ROLE_REQUEST = (
+ '/caravel/approve?datasource_type={}&datasource_id={}&'
+ 'created_by={}&role_to_grant={}')
+
+ # Case 1. Grant new role to the user.
+
+ access_request1 = prepare_request(
+ 'table', 'unicode_test', 'table_role')
+ ds_1_id = access_request1.datasource_id
+ self.client.get(GRANT_ROLE_REQUEST.format(
+ 'table', ds_1_id, 'gamma', 'table_role'))
+ access_requests = self.get_access_requests('gamma', 'table', ds_1_id)
+ # request was removed
+ self.assertFalse(access_requests)
+ # user was granted table_role
+ user_roles = [r.name for r in sm.find_user('gamma').roles]
+ self.assertIn('table_role', user_roles)
+
+ # Case 2. Extend the role to have access to the table
+
+ access_request2 = prepare_request('table', 'long_lat', 'table_role')
+ ds_2_id = access_request2.datasource_id
+ long_lat_perm = access_request2.datasource.perm
+
+ self.client.get(EXTEND_ROLE_REQUEST.format(
+ 'table', access_request2.datasource_id, 'gamma', 'table_role'))
+ access_requests = self.get_access_requests('gamma', 'table', ds_2_id)
+ # request was removed
+ self.assertFalse(access_requests)
+ # table_role was extended to grant access to the long_lat table/
+ table_role = sm.find_role('table_role')
+ perm_view = sm.find_permission_view_menu(
+ 'datasource_access', long_lat_perm)
+ self.assertIn(perm_view, table_role.permissions)
+
+ # Case 3. Grant new role to the user to access the druid datasource.
+
+ sm.add_role('druid_role')
+ access_request3 = prepare_request('druid', 'druid_ds_1', 'druid_role')
+ self.client.get(GRANT_ROLE_REQUEST.format(
+ 'druid', access_request3.datasource_id, 'gamma', 'druid_role'))
+
+ # user was granted table_role
+ user_roles = [r.name for r in sm.find_user('gamma').roles]
+ self.assertIn('druid_role', user_roles)
+
+ # Case 4. Extend the role to have access to the druid datasource
+
+ access_request4 = prepare_request('druid', 'druid_ds_2', 'druid_role')
+ druid_ds_2_perm = access_request4.datasource.perm
+
+ self.client.get(EXTEND_ROLE_REQUEST.format(
+ 'druid', access_request4.datasource_id, 'gamma', 'druid_role'))
+ # druid_role was extended to grant access to the druid_access_ds_2
+ druid_role = sm.find_role('druid_role')
+ perm_view = sm.find_permission_view_menu(
+ 'datasource_access', druid_ds_2_perm)
+ self.assertIn(perm_view, druid_role.permissions)
+
+ # cleanup
+ gamma_user = sm.find_user(username='gamma')
+ gamma_user.roles.remove(sm.find_role('druid_role'))
+ gamma_user.roles.remove(sm.find_role('table_role'))
+ session.delete(sm.find_role('druid_role'))
+ session.delete(sm.find_role('table_role'))
+ session.commit()
+
+ def test_request_access(self):
+ session = db.session
+ self.login(username='gamma')
+ gamma_user = sm.find_user(username='gamma')
+ sm.add_role('dummy_role')
+ gamma_user.roles.append(sm.find_role('dummy_role'))
+ session.commit()
+
+ ACCESS_REQUEST = (
+ '/caravel/request_access?datasource_type={}&datasource_id={}')
+ ROLE_EXTEND_LINK = (
+ 'Extend {} Role')
+ ROLE_GRANT_LINK = (
+ 'Grant {} Role')
+
+ # Case 1. Request table access, there are no roles have this table.
+
+ table1 = session.query(models.SqlaTable).filter_by(
+ table_name='random_time_series').first()
+ table_1_id = table1.id
+
+ # request access to the table
+ self.client.get(ACCESS_REQUEST.format('table', table_1_id))
+
+ access_request1 = self.get_access_requests(
+ 'gamma', 'table', table_1_id)[0]
+ approve_link_1 = ROLE_EXTEND_LINK.format(
+ 'table', table_1_id, 'gamma', 'dummy_role', 'dummy_role')
+ self.assertEqual(
+ access_request1.user_roles,
+ ''.format(approve_link_1))
+ self.assertEqual(access_request1.roles_with_datasource, '')
+
+ # Case 2. Duplicate request.
+
+ self.client.get(ACCESS_REQUEST.format('table', table_1_id))
+ access_requests_2 = self.get_access_requests(
+ 'gamma', 'table', table_1_id)
+ self.assertEqual(len(access_requests_2), 1)
+
+ # Case 3. Request access, roles exist that contains the table.
+
+ # add table to the existing roles
+ table3 = session.query(models.SqlaTable).filter_by(
+ table_name='energy_usage').first()
+ table_3_id = table3.id
+ table3_perm = table3.perm
+
+ sm.add_role('energy_usage_role')
+ alpha_role = sm.find_role('Alpha')
+ sm.add_permission_role(
+ alpha_role,
+ sm.find_permission_view_menu('datasource_access', table3_perm))
+ sm.add_permission_role(
+ sm.find_role("energy_usage_role"),
+ sm.find_permission_view_menu('datasource_access', table3_perm))
+ session.commit()
+
+ self.client.get(ACCESS_REQUEST.format('table', table_3_id))
+
+ access_request3 = self.get_access_requests(
+ 'gamma', 'table', table_3_id)[0]
+ approve_link_3 = ROLE_GRANT_LINK.format(
+ 'table', table_3_id, 'gamma', 'energy_usage_role',
+ 'energy_usage_role')
+ self.assertEqual(access_request3.roles_with_datasource,
+ ''.format(approve_link_3))
+
+ # Case 4. Request druid access, there are no roles have this table.
+ druid_ds_4 = session.query(models.DruidDatasource).filter_by(
+ datasource_name='druid_ds_1').first()
+ druid_ds_4_id = druid_ds_4.id
+
+ # request access to the table
+ self.client.get(ACCESS_REQUEST.format('druid', druid_ds_4_id))
+ access_request4 = self.get_access_requests(
+ 'gamma', 'druid', druid_ds_4_id)[0]
+ approve_link_4 = ROLE_EXTEND_LINK.format(
+ 'druid', druid_ds_4_id, 'gamma', 'dummy_role', 'dummy_role')
+ self.assertEqual(
+ access_request4.user_roles,
+ ''.format(approve_link_4))
+
+ self.assertEqual(
+ access_request4.roles_with_datasource,
+ ''.format(access_request4.id))
+
+ # Case 5. Roles exist that contains the druid datasource.
+ # add druid ds to the existing roles
+ druid_ds_5 = session.query(models.DruidDatasource).filter_by(
+ datasource_name='druid_ds_2').first()
+ druid_ds_5_id = druid_ds_5.id
+ druid_ds_5_perm = druid_ds_5.perm
+
+ druid_ds_2_role = sm.add_role('druid_ds_2_role')
+ admin_role = sm.find_role('Admin')
+ sm.add_permission_role(
+ admin_role,
+ sm.find_permission_view_menu('datasource_access', druid_ds_5_perm))
+ sm.add_permission_role(
+ druid_ds_2_role,
+ sm.find_permission_view_menu('datasource_access', druid_ds_5_perm))
+ session.commit()
+
+ self.client.get(ACCESS_REQUEST.format('druid', druid_ds_5_id))
+ access_request5 = self.get_access_requests(
+ 'gamma', 'druid', druid_ds_5_id)[0]
+ approve_link_5 = ROLE_GRANT_LINK.format(
+ 'druid', druid_ds_5_id, 'gamma', 'druid_ds_2_role',
+ 'druid_ds_2_role')
+
+ self.assertEqual(access_request5.roles_with_datasource,
+ ''.format(approve_link_5))
+
+ # cleanup
+ gamma_user = sm.find_user(username='gamma')
+ gamma_user.roles.remove(sm.find_role('dummy_role'))
+ session.commit()
def test_druid_sync_from_config(self):
+ self.login()
cluster = models.DruidCluster(cluster_name="new_druid")
db.session.add(cluster)
db.session.commit()