Skip to content

Commit

Permalink
Implement permission request/approve flow. (#1095)
Browse files Browse the repository at this point in the history
* Implement permission request/approve flow

* Address the comments.

* Refactor the code to support multiple datasources.

* Reformat the queries.
  • Loading branch information
bkyryliuk committed Sep 22, 2016
1 parent b855e2f commit cbc70d3
Show file tree
Hide file tree
Showing 7 changed files with 604 additions and 45 deletions.
@@ -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')
75 changes: 75 additions & 0 deletions caravel/models.py
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -2000,10 +2006,79 @@ def to_dict(self):
'tempTable': self.tmp_table_name,
'userId': self.user_id,
}

@property
def name(self):
ts = datetime.now().isoformat()
ts = ts.replace('-', '').replace(':', '').split('.')[0]
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>'
24 changes: 24 additions & 0 deletions 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" %}
<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 %}
62 changes: 39 additions & 23 deletions caravel/utils.py
Expand Up @@ -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 <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
Expand All @@ -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)
Expand Down

0 comments on commit cbc70d3

Please sign in to comment.