From cbc70d373842419dfef9c6a7f8ab40fd4c27a395 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 22 Sep 2016 09:53:14 -0700 Subject: [PATCH] Implement permission request/approve flow. (#1095) * Implement permission request/approve flow * Address the comments. * Refactor the code to support multiple datasources. * Reformat the queries. --- .../5e4a03ef0bf0_add_request_access_model.py | 33 +++ caravel/models.py | 75 ++++++ caravel/templates/caravel/request_access.html | 24 ++ caravel/utils.py | 62 +++-- caravel/views.py | 170 ++++++++++-- tests/base_tests.py | 33 ++- tests/core_tests.py | 252 +++++++++++++++++- 7 files changed, 604 insertions(+), 45 deletions(-) create mode 100644 caravel/migrations/versions/5e4a03ef0bf0_add_request_access_model.py create mode 100644 caravel/templates/caravel/request_access.html 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 0000000000..ad6375f183 --- /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 c59b49a955..38a457b98d 100644 --- a/caravel/models.py +++ b/caravel/models.py @@ -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 = '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 0000000000..0c075bac23 --- /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 9e2c7f8d78..c80b94fc81 100644 --- a/caravel/utils.py +++ b/caravel/utils.py @@ -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 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 3aa87f7070..d87c9ba8a6 100755 --- a/caravel/views.py +++ b/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///" + "") + 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 @@ 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( diff --git a/tests/base_tests.py b/tests/base_tests.py index 4cae61bf12..dc781f9378 100644 --- a/tests/base_tests.py +++ b/tests/base_tests.py @@ -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) diff --git a/tests/core_tests.py b/tests/core_tests.py index 2b24357997..1297cfb308 100644 --- a/tests/core_tests.py +++ b/tests/core_tests.py @@ -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 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 @@ 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 = ( + '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, + '
    • Gamma Role
    • {}
    '.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, + '
      • Gamma Role
      • {}
      '.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()