Implement permission request/approve flow. (#1095)

* Implement permission request/approve flow

* Address the comments.

* Refactor the code to support multiple datasources.

* Reformat the queries.
This commit is contained in:
Bogdan 2016-09-22 09:53:14 -07:00 committed by GitHub
parent b855e2f1a6
commit cbc70d3738
7 changed files with 604 additions and 45 deletions

View File

@ -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')

View File

@ -26,6 +26,7 @@ from flask import escape, g, Markup, request
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 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
"[{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 @@ class DruidCluster(Model, AuditMixinNullable):
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 @@ class Query(Model):
'tempTable': self.tmp_table_name,
'userId': self.user_id,
}
@property
def name(self):
ts = datetime.now().isoformat()
@ -2007,3 +2014,71 @@ class Query(Model):
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 = '<a href="{}">Grant {} Role</a>'.format(url, r.name)
action_list = action_list + '<li>' + href + '</li>'
return '<ul>' + action_list + '</ul>'
@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 = '<a href="{}">Extend {} Role</a>'.format(url, r.name)
if r.name in self.ROLES_BLACKLIST:
href = "{} Role".format(r.name)
action_list = action_list + '<li>' + href + '</li>'
return '<ul>' + action_list + '</ul>'

View File

@ -0,0 +1,24 @@
{% extends "caravel/basic.html" %}
{% block title %}{{ _("No Access!") }}{% endblock %}
{% block body %}
{% include "caravel/flash_wrapper.html" %}
<div class="container">
<h4>
{{ _("You do not have permissions to access the datasource %(name)s.",
name=datasource_name)
}}
</h4>
<div id="buttons">
<button onclick="window.location.href = '{{ request_access_url }}';"
id="request"
>
{{ _("Request Permissions") }}
</button>
<button onclick="window.location.href = '{{ slicemodelview_link }}';"
id="cancel"
>
{{ _("Cancel") }}
</button>
</div>
</div>
{% endblock %}

View File

@ -212,6 +212,32 @@ class JSONEncodedDict(TypeDecorator):
def init(caravel):
"""Inits the Caravel application with security roles and such"""
ADMIN_ONLY_VIEW_MENUES = set([
'ResetPasswordView',
'RoleModelView',
'Security',
'UserDBModelView',
'SQL Lab <span class="label label-danger">alpha</span>',
'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)

View File

@ -36,6 +36,7 @@ from caravel import (
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 @@ appbuilder.add_view(
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 @@ appbuilder.add_view_no_menu(R)
class Caravel(BaseCaravelView):
"""The base views for Caravel!"""
@log_this
@has_access
@expose("/request_access_form/<datasource_type>/<datasource_id>/"
"<datasource_name>")
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/<datasource_type>/<datasource_id>/<slice_id>/")
@expose("/explore/<datasource_type>/<datasource_id>/")
@ -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 @@ class Caravel(BaseCaravelView):
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 @@ class Caravel(BaseCaravelView):
# 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(

View File

@ -10,7 +10,7 @@ import unittest
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 @@ class CaravelTestCase(unittest.TestCase):
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 @@ class CaravelTestCase(unittest.TestCase):
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)

View File

@ -17,7 +17,7 @@ from flask import escape
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 @@ from .base_tests import CaravelTestCase
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 @@ class CoreTests(CaravelTestCase):
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 <span class="label label-danger">alpha</span>',
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 @@ class CoreTests(CaravelTestCase):
"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 @@ class CoreTests(CaravelTestCase):
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 = (
'<a href="/caravel/approve?datasource_type={}&datasource_id={}&'
'created_by={}&role_to_extend={}">Extend {} Role</a>')
ROLE_GRANT_LINK = (
'<a href="/caravel/approve?datasource_type={}&datasource_id={}&'
'created_by={}&role_to_grant={}">Grant {} Role</a>')
# 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,
'<ul><li>Gamma Role</li><li>{}</li></ul>'.format(approve_link_1))
self.assertEqual(access_request1.roles_with_datasource, '<ul></ul>')
# 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,
'<ul><li>{}</li></ul>'.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,
'<ul><li>Gamma Role</li><li>{}</li></ul>'.format(approve_link_4))
self.assertEqual(
access_request4.roles_with_datasource,
'<ul></ul>'.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,
'<ul><li>{}</li></ul>'.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()