mirror of https://github.com/apache/superset.git
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:
parent
b855e2f1a6
commit
cbc70d3738
|
@ -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')
|
|
@ -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>'
|
||||
|
|
|
@ -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 %}
|
|
@ -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)
|
||||
|
|
170
caravel/views.py
170
caravel/views.py
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue