Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement permission request/approve flow. #1095

Merged
merged 5 commits into from
Sep 22, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

related to my other comment about auditing who granted what to who, I'd add:

state as ('requested', 'granted' and maybe eventually 'denied')
processed_by_userid Integer
processed_on Datetime

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've refactored the code in a way that @log_this will have all needed information

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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably need a new role for people who can grant rights to others that are not admin. It doesn't have to be part of this PR, your call.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree, separate PR

])

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