Override the role with perms for given datasources. (#1399)

* Override the role with perms for give datasources.

* Address comments.
This commit is contained in:
Bogdan 2016-10-20 15:30:09 -07:00 committed by GitHub
parent c198535292
commit 5c3966a32d
7 changed files with 209 additions and 7 deletions

View File

@ -173,7 +173,6 @@ DRUID_DATA_SOURCE_BLACKLIST = []
DEFAULT_MODULE_DS_MAP = {'caravel.models': ['DruidDatasource', 'SqlaTable']}
ADDITIONAL_MODULE_DS_MAP = {}
"""
1) http://docs.python-guide.org/en/latest/writing/logging/
2) https://docs.python.org/2/library/logging.config.html

View File

@ -635,6 +635,7 @@ class Database(Model, AuditMixinNullable):
"""An ORM object that stores Database related information"""
__tablename__ = 'dbs'
id = Column(Integer, primary_key=True)
database_name = Column(String(250), unique=True)
sqlalchemy_uri = Column(String(1024))
@ -859,7 +860,8 @@ class SqlaTable(Model, Queryable, AuditMixinNullable, ImportMixin):
@property
def full_name(self):
return "[{obj.database}].[{obj.table_name}]".format(obj=self)
return utils.get_datasource_full_name(
self.database, self.table_name, schema=self.schema)
@property
def dttm_cols(self):
@ -1435,6 +1437,7 @@ class DruidCluster(Model, AuditMixinNullable):
"""ORM object referencing the Druid clusters"""
__tablename__ = 'clusters'
id = Column(Integer, primary_key=True)
cluster_name = Column(String(250), unique=True)
coordinator_host = Column(String(255))
@ -1547,9 +1550,8 @@ class DruidDatasource(Model, AuditMixinNullable, Queryable):
@property
def full_name(self):
return (
"[{obj.cluster_name}]."
"[{obj.datasource_name}]").format(obj=self)
return utils.get_datasource_full_name(
self.cluster_name, self.datasource_name)
@property
def time_column_grains(self):

View File

@ -22,6 +22,14 @@ class SourceRegistry(object):
.one()
)
@classmethod
def get_all_datasources(cls, session):
datasources = []
for source_type in SourceRegistry.sources:
datasources.extend(
session.query(SourceRegistry.sources[source_type]).all())
return datasources
@classmethod
def get_datasource_by_name(cls, session, datasource_type, datasource_name,
schema, database_name):

View File

@ -230,6 +230,7 @@ def init(caravel):
ADMIN_ONLY_PERMISSIONS = set([
'can_sync_druid_source',
'can_override_role_permissions',
'can_approve',
])
@ -447,6 +448,12 @@ def generic_find_constraint_name(table, columns, referenced, db):
return fk.name
def get_datasource_full_name(database_name, datasource_name, schema=None):
if not schema:
return "[{}].[{}]".format(database_name, datasource_name)
return "[{}].[{}].[{}]".format(database_name, schema, datasource_name)
def validate_json(obj):
if obj:
try:

View File

@ -1060,6 +1060,53 @@ appbuilder.add_view_no_menu(R)
class Caravel(BaseCaravelView):
"""The base views for Caravel!"""
@has_access
@expose("/override_role_permissions/", methods=['POST'])
def override_role_permissions(self):
"""Updates the role with the give datasource permissions.
Permissions not in the request will be revoked. This endpoint should
be available to admins only. Expects JSON in the format:
{
'role_name': '{role_name}',
'database': [{
'datasource_type': '{table|druid}',
'name': '{database_name}',
'schema': [{
'name': '{schema_name}',
'datasources': ['{datasource name}, {datasource name}']
}]
}]
}
"""
data = request.get_json(force=True)
role_name = data['role_name']
databases = data['database']
db_ds_names = set()
for dbs in databases:
for schema in dbs['schema']:
for ds_name in schema['datasources']:
db_ds_names.add(utils.get_datasource_full_name(
dbs['name'], ds_name, schema=schema['name']))
existing_datasources = SourceRegistry.get_all_datasources(db.session)
datasources = [
d for d in existing_datasources if d.full_name in db_ds_names]
role = sm.find_role(role_name)
# remove all permissions
role.permissions = []
# grant permissions to the list of datasources
for ds_name in datasources:
role.permissions.append(
sm.find_permission_view_menu(
view_menu_name=ds_name.perm,
permission_name='datasource_access')
)
db.session.commit()
return Response(status=201)
@log_this
@has_access
@expose("/request_access/")

View File

@ -4,6 +4,7 @@ from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import json
import unittest
from caravel import db, models, sm
@ -11,16 +12,144 @@ from caravel.source_registry import SourceRegistry
from .base_tests import CaravelTestCase
ROLE_TABLES_PERM_DATA = {
'role_name': 'override_me',
'database': [{
'datasource_type': 'table',
'name': 'main',
'schema': [{
'name': '',
'datasources': ['birth_names']
}]
}]
}
ROLE_ALL_PERM_DATA = {
'role_name': 'override_me',
'database': [{
'datasource_type': 'table',
'name': 'main',
'schema': [{
'name': '',
'datasources': ['birth_names']
}]
}, {
'datasource_type': 'druid',
'name': 'druid_test',
'schema': [{
'name': '',
'datasources': ['druid_ds_1', 'druid_ds_2']
}]
}
]
}
class RequestAccessTests(CaravelTestCase):
requires_examples = True
requires_examples = False
@classmethod
def setUpClass(cls):
sm.add_role('override_me')
db.session.commit()
@classmethod
def tearDownClass(cls):
override_me = sm.find_role('override_me')
db.session.delete(override_me)
db.session.commit()
def setUp(self):
self.login('admin')
def tearDown(self):
self.logout()
override_me = sm.find_role('override_me')
override_me.permissions = []
db.session.commit()
db.session.close()
def test_override_role_permissions_is_admin_only(self):
self.logout()
self.login('alpha')
response = self.client.post(
'/caravel/override_role_permissions/',
data=json.dumps(ROLE_TABLES_PERM_DATA),
content_type='application/json',
follow_redirects=True)
self.assertNotEquals(405, response.status_code)
def test_override_role_permissions_1_table(self):
response = self.client.post(
'/caravel/override_role_permissions/',
data=json.dumps(ROLE_TABLES_PERM_DATA),
content_type='application/json')
self.assertEquals(201, response.status_code)
updated_override_me = sm.find_role('override_me')
self.assertEquals(1, len(updated_override_me.permissions))
birth_names = self.get_table_by_name('birth_names')
self.assertEquals(
birth_names.perm,
updated_override_me.permissions[0].view_menu.name)
self.assertEquals(
'datasource_access',
updated_override_me.permissions[0].permission.name)
def test_override_role_permissions_druid_and_table(self):
response = self.client.post(
'/caravel/override_role_permissions/',
data=json.dumps(ROLE_ALL_PERM_DATA),
content_type='application/json')
self.assertEquals(201, response.status_code)
updated_role = sm.find_role('override_me')
perms = sorted(
updated_role.permissions, key=lambda p: p.view_menu.name)
self.assertEquals(3, len(perms))
druid_ds_1 = self.get_druid_ds_by_name('druid_ds_1')
self.assertEquals(druid_ds_1.perm, perms[0].view_menu.name)
self.assertEquals('datasource_access', perms[0].permission.name)
druid_ds_2 = self.get_druid_ds_by_name('druid_ds_2')
self.assertEquals(druid_ds_2.perm, perms[1].view_menu.name)
self.assertEquals(
'datasource_access', updated_role.permissions[1].permission.name)
birth_names = self.get_table_by_name('birth_names')
self.assertEquals(birth_names.perm, perms[2].view_menu.name)
self.assertEquals(
'datasource_access', updated_role.permissions[2].permission.name)
def test_override_role_permissions_drops_absent_perms(self):
override_me = sm.find_role('override_me')
override_me.permissions.append(
sm.find_permission_view_menu(
view_menu_name=self.get_table_by_name('long_lat').perm,
permission_name='datasource_access')
)
db.session.flush()
response = self.client.post(
'/caravel/override_role_permissions/',
data=json.dumps(ROLE_TABLES_PERM_DATA),
content_type='application/json')
self.assertEquals(201, response.status_code)
updated_override_me = sm.find_role('override_me')
self.assertEquals(1, len(updated_override_me.permissions))
birth_names = self.get_table_by_name('birth_names')
self.assertEquals(
birth_names.perm,
updated_override_me.permissions[0].view_menu.name)
self.assertEquals(
'datasource_access',
updated_override_me.permissions[0].permission.name)
def test_approve(self):
session = db.session
TEST_ROLE_NAME = 'table_role'
sm.add_role(TEST_ROLE_NAME)
self.login('admin')
def create_access_request(ds_type, ds_name, role_name):
ds_class = SourceRegistry.sources[ds_type]
@ -116,6 +245,7 @@ class RequestAccessTests(CaravelTestCase):
def test_request_access(self):
session = db.session
self.logout()
self.login(username='gamma')
gamma_user = sm.find_user(username='gamma')
sm.add_role('dummy_role')

View File

@ -120,6 +120,15 @@ class CaravelTestCase(unittest.TestCase):
session.expunge_all()
return slc
def get_table_by_name(self, name):
return db.session.query(models.SqlaTable).filter_by(
table_name=name).first()
def get_druid_ds_by_name(self, name):
return db.session.query(models.DruidDatasource).filter_by(
datasource_name=name).first()
def get_resp(self, url):
"""Shortcut to get the parsed results while following redirects"""
resp = self.client.get(url, follow_redirects=True)