Override the role with perms for given datasources. (#1399)
* Override the role with perms for give datasources.
* Address comments.
diff --git a/caravel/config.py b/caravel/config.py
index 89bcaa7..71b7542 100644
--- a/caravel/config.py
+++ b/caravel/config.py
@@ -173,7 +173,6 @@
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
diff --git a/caravel/models.py b/caravel/models.py
index ddfdee2..fdb2c3a 100644
--- a/caravel/models.py
+++ b/caravel/models.py
@@ -635,6 +635,7 @@
"""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 @@
@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 @@
"""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 @@
@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):
diff --git a/caravel/source_registry.py b/caravel/source_registry.py
index 6176c9c..669ca17 100644
--- a/caravel/source_registry.py
+++ b/caravel/source_registry.py
@@ -23,6 +23,14 @@
)
@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):
datasource_class = SourceRegistry.sources[datasource_type]
diff --git a/caravel/utils.py b/caravel/utils.py
index 7f8d33b..f685ec5 100644
--- a/caravel/utils.py
+++ b/caravel/utils.py
@@ -230,6 +230,7 @@
ADMIN_ONLY_PERMISSIONS = set([
'can_sync_druid_source',
+ 'can_override_role_permissions',
'can_approve',
])
@@ -447,6 +448,12 @@
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:
diff --git a/caravel/views.py b/caravel/views.py
index fc16e97..5ab1dd4 100755
--- a/caravel/views.py
+++ b/caravel/views.py
@@ -1060,6 +1060,53 @@
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/")
diff --git a/tests/access_requests.py b/tests/access_tests.py
similarity index 66%
rename from tests/access_requests.py
rename to tests/access_tests.py
index f443c27..65e6a9a 100644
--- a/tests/access_requests.py
+++ b/tests/access_tests.py
@@ -4,6 +4,7 @@
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 .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 @@
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')
diff --git a/tests/base_tests.py b/tests/base_tests.py
index 20241ea..45abf84 100644
--- a/tests/base_tests.py
+++ b/tests/base_tests.py
@@ -120,6 +120,15 @@
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)