Browse Source

HUE-8758 [connectors] Add permission to connector listing

Added a new test class
Romain 5 years ago
parent
commit
4cbdcf0f93

+ 14 - 11
apps/useradmin/src/useradmin/models.py

@@ -115,6 +115,9 @@ class UserProfile(models.Model):
     group_ids = self.user.groups.values_list('id', flat=True)
     group_ids = self.user.groups.values_list('id', flat=True)
     return GroupPermission.objects.filter(group__id__in=group_ids, hue_permission=perm).exists()
     return GroupPermission.objects.filter(group__id__in=group_ids, hue_permission=perm).exists()
 
 
+  def get_permissions(self):
+    return HuePermission.objects.filter(groups__user=self.user)
+
   def check_hue_permission(self, perm=None, app=None, action=None):
   def check_hue_permission(self, perm=None, app=None, action=None):
     """
     """
     Raises a PopupException if permission is denied.
     Raises a PopupException if permission is denied.
@@ -274,7 +277,7 @@ def update_app_permissions(**kwargs):
 
 
     for app in apps:
     for app in apps:
       app_name = app.name
       app_name = app.name
-      permission_description = "Use the connector %s" % app.nice_name if ENABLE_CONNECTORS.get() else "Launch this application"
+      permission_description = "Access the %s connection" % app.nice_name if ENABLE_CONNECTORS.get() else "Launch this application"
       actions = set([("access", permission_description)])
       actions = set([("access", permission_description)])
       actions.update(getattr(app.settings, "PERMISSION_ACTIONS", []))
       actions.update(getattr(app.settings, "PERMISSION_ACTIONS", []))
 
 
@@ -300,20 +303,20 @@ def update_app_permissions(**kwargs):
     # Only with v2
     # Only with v2
     deleted, _ = HuePermission.objects.filter(app__in=old_apps).delete()
     deleted, _ = HuePermission.objects.filter(app__in=old_apps).delete()
 
 
-    # Add all permissions to default group except some.
+    # Add all permissions to default group except some
     default_group = get_default_user_group()
     default_group = get_default_user_group()
     if default_group:
     if default_group:
       for new_dp in added:
       for new_dp in added:
         if not (new_dp.app == 'useradmin' and new_dp.action == 'access') and \
         if not (new_dp.app == 'useradmin' and new_dp.action == 'access') and \
-           not (new_dp.app == 'useradmin' and new_dp.action == 'superuser') and \
-           not (new_dp.app == 'metastore' and new_dp.action == 'write') and \
-           not (new_dp.app == 'hbase' and new_dp.action == 'write') and \
-           not (new_dp.app == 'security' and new_dp.action == 'impersonate') and \
-           not (new_dp.app == 'filebrowser' and new_dp.action == 's3_access' and not is_idbroker_enabled('s3a')) and \
-           not (new_dp.app == 'filebrowser' and new_dp.action == 'gs_access' and not is_idbroker_enabled('gs')) and \
-           not (new_dp.app == 'filebrowser' and new_dp.action == 'adls_access') and \
-           not (new_dp.app == 'filebrowser' and new_dp.action == 'abfs_access') and \
-           not (new_dp.app == 'oozie' and new_dp.action == 'disable_editor_access'):
+            not (new_dp.app == 'useradmin' and new_dp.action == 'superuser') and \
+            not (new_dp.app == 'metastore' and new_dp.action == 'write') and \
+            not (new_dp.app == 'hbase' and new_dp.action == 'write') and \
+            not (new_dp.app == 'security' and new_dp.action == 'impersonate') and \
+            not (new_dp.app == 'filebrowser' and new_dp.action == 's3_access' and not is_idbroker_enabled('s3a')) and \
+            not (new_dp.app == 'filebrowser' and new_dp.action == 'gs_access' and not is_idbroker_enabled('gs')) and \
+            not (new_dp.app == 'filebrowser' and new_dp.action == 'adls_access') and \
+            not (new_dp.app == 'filebrowser' and new_dp.action == 'abfs_access') and \
+            not (new_dp.app == 'oozie' and new_dp.action == 'disable_editor_access'):
           GroupPermission.objects.create(group=default_group, hue_permission=new_dp)
           GroupPermission.objects.create(group=default_group, hue_permission=new_dp)
 
 
     available = HuePermission.objects.count()
     available = HuePermission.objects.count()

+ 10 - 10
apps/useradmin/src/useradmin/templates/edit_user.mako

@@ -27,7 +27,7 @@ from useradmin.views import is_user_locked_out
 <%namespace name="layout" file="layout.mako" />
 <%namespace name="layout" file="layout.mako" />
 
 
 % if not is_embeddable:
 % if not is_embeddable:
-${ commonheader(_('Users'), "useradmin", user, request) | n,unicode }
+  ${ commonheader(_('Users'), "useradmin", user, request) | n,unicode }
 % endif
 % endif
 
 
 ${ layout.menubar(section='users') }
 ${ layout.menubar(section='users') }
@@ -103,15 +103,15 @@ ${ layout.menubar(section='users') }
             ${layout.render_field(form["groups"])}
             ${layout.render_field(form["groups"])}
           % endif
           % endif
         </div>
         </div>
-      % if is_admin(user):
-        <div id="step3" class="stepDetails hide">
-          ${ layout.render_field(form["is_active"]) }
-          ${'is_superuser' in form.fields and layout.render_field(form["is_superuser"])}
-          % if is_user_locked_out(username):
-            ${ layout.render_field(form["unlock_account"]) }
-          % endif
-        </div>
-      % endif
+        % if is_admin(user):
+          <div id="step3" class="stepDetails hide">
+            ${ layout.render_field(form["is_active"]) }
+            ${ 'is_superuser' in form.fields and layout.render_field(form["is_superuser"]) }
+            % if is_user_locked_out(username):
+              ${ layout.render_field(form["unlock_account"]) }
+            % endif
+          </div>
+        % endif
       </div>
       </div>
 
 
       <div class="form-actions">
       <div class="form-actions">

+ 1 - 1
apps/useradmin/src/useradmin/templates/list_permissions.mako

@@ -54,7 +54,7 @@ from useradmin.models import group_permissions, Group
             <td>
             <td>
               % if is_admin(user):
               % if is_admin(user):
                 <strong>
                 <strong>
-                  <a title="${_('Edit permission')}"
+                  <a title="${ _('Edit permission') }"
                       href="${ url('useradmin.views.edit_permission', app=perm.app, priv=perm.action) }"
                       href="${ url('useradmin.views.edit_permission', app=perm.app, priv=perm.action) }"
                       data-name="${ perm.app }" data-row-selector="true">${ perm.app }
                       data-name="${ perm.app }" data-row-selector="true">${ perm.app }
                   </a>
                   </a>

+ 4 - 1
desktop/core/src/desktop/auth/backend.py

@@ -94,7 +94,7 @@ def rewrite_user(user):
     LOG.warn('Failed to rewrite user, user is None.')
     LOG.warn('Failed to rewrite user, user is None.')
   else:
   else:
     augment = get_user_augmentation_class()(user)
     augment = get_user_augmentation_class()(user)
-    for attr in ("get_groups", "get_home_directory", "has_hue_permission"):
+    for attr in ('get_groups', 'get_home_directory', 'has_hue_permission', 'get_permissions'):
       setattr(user, attr, getattr(augment, attr))
       setattr(user, attr, getattr(augment, attr))
 
 
     profile_data = get_profile(user).data
     profile_data = get_profile(user).data
@@ -135,6 +135,9 @@ class DefaultUserAugmentor(object):
   def has_hue_permission(self, action, app):
   def has_hue_permission(self, action, app):
     return self._get_profile().has_hue_permission(action=action, app=app)
     return self._get_profile().has_hue_permission(action=action, app=app)
 
 
+  def get_permissions(self):
+    return self._get_profile().get_permissions()
+
 
 
 def find_user(username):
 def find_user(username):
   lookup = {'email': username} if ENABLE_ORGANIZATIONS.get() else {'username': username}
   lookup = {'email': username} if ENABLE_ORGANIZATIONS.get() else {'username': username}

+ 5 - 1
desktop/core/src/desktop/lib/connectors/models.py

@@ -24,6 +24,7 @@ from django.utils.translation import ugettext as _, ugettext_lazy as _t
 
 
 from desktop.conf import CONNECTORS
 from desktop.conf import CONNECTORS
 from desktop.lib.connectors.types import CONNECTOR_TYPES, CATEGORIES
 from desktop.lib.connectors.types import CONNECTOR_TYPES, CATEGORIES
+from desktop.lib.exceptions_renderable import PopupException
 
 
 
 
 LOG = logging.getLogger(__name__)
 LOG = logging.getLogger(__name__)
@@ -67,7 +68,7 @@ def _group_category_connectors(connectors):
 AVAILABLE_CONNECTORS = _group_category_connectors(CONNECTOR_TYPES)
 AVAILABLE_CONNECTORS = _group_category_connectors(CONNECTOR_TYPES)
 
 
 
 
-def _get_installed_connectors(category=None, categories=None, dialect=None, interface=None):
+def _get_installed_connectors(category=None, categories=None, dialect=None, interface=None, user=None):
   global CONNECTOR_INSTANCES
   global CONNECTOR_INSTANCES
   global CONNECTOR_IDS
   global CONNECTOR_IDS
   config_connectors = CONNECTORS.get()
   config_connectors = CONNECTORS.get()
@@ -115,6 +116,9 @@ def _get_installed_connectors(category=None, categories=None, dialect=None, inte
     connectors = [connector for connector in connectors if connector['dialect'] == dialect]
     connectors = [connector for connector in connectors if connector['dialect'] == dialect]
   if interface is not None:
   if interface is not None:
     connectors = [connector for connector in connectors if connector['interface'] == interface]
     connectors = [connector for connector in connectors if connector['interface'] == interface]
+  if user is not None:
+    allowed_connectors = user.get_permissions().values_list('app', flat=True)
+    connectors = [connector for connector in connectors if connector['name'] in allowed_connectors]
 
 
   return connectors
   return connectors
 
 

+ 61 - 21
desktop/core/src/desktop/lib/connectors/tests.py

@@ -19,11 +19,11 @@ import sys
 
 
 from nose.tools import assert_equal, assert_true, assert_false
 from nose.tools import assert_equal, assert_true, assert_false
 
 
+from desktop.auth.backend import rewrite_user
 from desktop.lib.connectors.api import _get_installed_connectors
 from desktop.lib.connectors.api import _get_installed_connectors
 from desktop.lib.django_test_util import make_logged_in_client
 from desktop.lib.django_test_util import make_logged_in_client
-from desktop.lib.test_utils import grant_access
 
 
-from useradmin.models import User
+from useradmin.models import User, update_app_permissions, get_default_user_group
 
 
 if sys.version_info[0] > 2:
 if sys.version_info[0] > 2:
   from unittest.mock import patch, Mock
   from unittest.mock import patch, Mock
@@ -36,35 +36,75 @@ class TestConnectors(object):
   def setUp(self):
   def setUp(self):
     self.client = make_logged_in_client(username="test_connector", recreate=True, is_superuser=False)
     self.client = make_logged_in_client(username="test_connector", recreate=True, is_superuser=False)
     self.user = User.objects.get(username="test_connector")
     self.user = User.objects.get(username="test_connector")
-    grant_access(self.user.username, self.user.username, "desktop")
-
 
 
   def test_page(self):
   def test_page(self):
     response = self.client.get("/desktop/connectors/")
     response = self.client.get("/desktop/connectors/")
 
 
     assert_equal(200, response.status_code)
     assert_equal(200, response.status_code)
 
 
-
   def test_get_connector_types(self):
   def test_get_connector_types(self):
     response = self.client.post("/desktop/connectors/api/types/")
     response = self.client.post("/desktop/connectors/api/types/")
 
 
     assert_equal(200, response.status_code)
     assert_equal(200, response.status_code)
 
 
 
 
-def test_get_installed_editor_connectors():
-
-  with patch('desktop.lib.connectors.api.CONNECTORS.get') as CONNECTORS:
-    CONNECTORS.return_value = {
-      'mysql-1': Mock(
-        NICE_NAME=Mock(get=Mock(return_value='MySql')),
-        DIALECT=Mock(get=Mock(return_value='mysql')),
-        INTERFACE=Mock(get=Mock(return_value='sqlalchemy')),
-        SETTINGS=Mock(get=Mock(return_value=[{"name": "url", "value": "mysql://hue:pwd@hue:3306/hue"}])),
-      )
-    }
+class TestConnectorListing():
 
 
-    connectors = _get_installed_connectors()
-
-    editor_category = [category for category in connectors if category['category'] == 'editor']
-    assert_true(len(editor_category), connectors)
-    assert_equal(1, len(editor_category), editor_category)
+  def setUp(self):
+    self.client = make_logged_in_client(
+        username='test_connector',
+        groupname=get_default_user_group(),
+        recreate=True,
+        is_superuser=False
+    )
+    self.user = User.objects.get(username='test_connector')
+    self.user = rewrite_user(self.user)
+
+    self.alone_client = make_logged_in_client(
+        username='test_alone',
+        groupname='alone',  # Not in default group
+        recreate=True,
+        is_superuser=False
+    )
+    self.alone_user = User.objects.get(username='test_alone')
+    self.alone_user = rewrite_user(self.alone_user)
+
+  @patch('desktop.lib.connectors.models.CONNECTOR_INSTANCES', None)
+  def test_get_installed_editor_connectors(self):
+
+    with patch('desktop.lib.connectors.models.CONNECTORS.get') as CONNECTORS:
+      CONNECTORS.return_value = {
+        'mysql-1': Mock(
+          NICE_NAME=Mock(get=Mock(return_value='MySql')),
+          DIALECT=Mock(get=Mock(return_value='mysql')),
+          INTERFACE=Mock(get=Mock(return_value='sqlalchemy')),
+          SETTINGS=Mock(get=Mock(return_value=[{"name": "url", "value": "mysql://hue:pwd@hue:3306/hue"}])),
+        )
+      }
+
+      connectors = _get_installed_connectors()
+
+      editor_category = [category for category in connectors if category['category'] == 'editor']
+      assert_true(editor_category, connectors)
+      assert_equal(1, len(editor_category), editor_category)
+
+  @patch('desktop.lib.connectors.models.CONNECTOR_INSTANCES', None)
+  def test_get_connectors_for_user(self):
+
+    with patch('desktop.lib.connectors.models.CONNECTORS.get') as CONNECTORS:
+      CONNECTORS.return_value = {
+        'mysql-1': Mock(
+          NICE_NAME=Mock(get=Mock(return_value='MySql')),
+          DIALECT=Mock(get=Mock(return_value='mysql')),
+          INTERFACE=Mock(get=Mock(return_value='sqlalchemy')),
+          SETTINGS=Mock(get=Mock(return_value=[{"name": "url", "value": "mysql://hue:pwd@hue:3306/hue"}])),
+        )
+      }
+
+      update_app_permissions()
+
+      connectors = _get_installed_connectors(user=self.user)
+      assert_true(connectors, connectors)
+
+      connectors = _get_installed_connectors(user=self.alone_user)
+      assert_false(connectors, connectors)

+ 11 - 2
desktop/core/src/desktop/lib/connectors/types.py

@@ -40,7 +40,8 @@ CONNECTOR_TYPES = [
     ],
     ],
     'properties': {
     'properties': {
       'is_sql': True,
       'is_sql': True,
-      'sql_identifiers_quote': '`',
+      'sql_identifier_quote': '`',
+      'sql_identifier_comment_single': '--',
       'has_catalog': True,
       'has_catalog': True,
       'has_database': True,
       'has_database': True,
       'has_table': True,
       'has_table': True,
@@ -75,7 +76,15 @@ CONNECTOR_TYPES = [
   {'nice_name': "Kafka SQL", 'dialect': 'ksql', 'interface': 'ksql', 'settings': [], 'category': 'editor', 'description': '', 'properties': {'is_sql': True}},
   {'nice_name': "Kafka SQL", 'dialect': 'ksql', 'interface': 'ksql', 'settings': [], 'category': 'editor', 'description': '', 'properties': {'is_sql': True}},
   {'nice_name': "Flink SQL", 'dialect': 'flink', 'interface': 'flink', 'settings': [{'name': 'api_url', 'value': 'http://flink:10000'}], 'category': 'editor', 'description': '', 'properties': {'is_sql': True}},
   {'nice_name': "Flink SQL", 'dialect': 'flink', 'interface': 'flink', 'settings': [{'name': 'api_url', 'value': 'http://flink:10000'}], 'category': 'editor', 'description': '', 'properties': {'is_sql': True}},
   {'nice_name': "SparkSQL", 'dialect': 'spark-sql', 'interface': 'sqlalchemy', 'settings': [], 'category': 'editor', 'description': '', 'properties': {'is_sql': True}},
   {'nice_name': "SparkSQL", 'dialect': 'spark-sql', 'interface': 'sqlalchemy', 'settings': [], 'category': 'editor', 'description': '', 'properties': {'is_sql': True}},
-  {'nice_name': "MySQL", 'dialect': 'mysql', 'interface': 'sqlalchemy', 'settings': [{'name': 'url', 'value': 'mysql://username:password@mysq-host:3306/hue'}], 'category': 'editor', 'description': '', 'properties': {'is_sql': True}},
+  {
+    'nice_name': "MySQL",
+    'dialect': 'mysql',
+    'interface': 'sqlalchemy',
+    'settings': [{'name': 'url', 'value': 'mysql://username:password@mysq-host:3306/hue'}],
+    'category': 'editor',
+    'description': '',
+    'properties': {'is_sql': True}
+  },
   {'nice_name': "Presto", 'dialect': 'presto', 'interface': 'sqlalchemy', 'settings': [], 'category': 'editor', 'description': '', 'properties': {'is_sql': True}},
   {'nice_name': "Presto", 'dialect': 'presto', 'interface': 'sqlalchemy', 'settings': [], 'category': 'editor', 'description': '', 'properties': {'is_sql': True}},
   {'nice_name': "Athena", 'dialect': 'athena', 'interface': 'sqlalchemy', 'settings': [], 'category': 'editor', 'description': '', 'properties': {'is_sql': True}},
   {'nice_name': "Athena", 'dialect': 'athena', 'interface': 'sqlalchemy', 'settings': [], 'category': 'editor', 'description': '', 'properties': {'is_sql': True}},
   {'nice_name': "Redshift", 'dialect': 'redshift', 'interface': 'sqlalchemy', 'settings': [], 'category': 'editor', 'description': '', 'properties': {'is_sql': True}},
   {'nice_name': "Redshift", 'dialect': 'redshift', 'interface': 'sqlalchemy', 'settings': [], 'category': 'editor', 'description': '', 'properties': {'is_sql': True}},

+ 0 - 1
desktop/libs/liboauth/src/liboauth/backend.py

@@ -41,7 +41,6 @@ except:
   oauth = None
   oauth = None
 
 
 if sys.version_info[0] > 2:
 if sys.version_info[0] > 2:
-  import urllib.request, urllib.parse, urllib.error
   from urllib.parse import urlencode as lib_urlencode
   from urllib.parse import urlencode as lib_urlencode
 else:
 else:
   from urllib import urlencode as lib_urlencode
   from urllib import urlencode as lib_urlencode

+ 2 - 3
desktop/libs/notebook/src/notebook/conf.py

@@ -57,9 +57,8 @@ def get_ordered_interpreters(user=None):
         'is_sql': connector.get('is_sql', False),
         'is_sql': connector.get('is_sql', False),
         'interface': connector['interface'],
         'interface': connector['interface'],
         'options': {setting['name']: setting['value'] for setting in connector['settings']}
         'options': {setting['name']: setting['value'] for setting in connector['settings']}
-      } for connector in _get_installed_connectors(categories=['editor', 'catalogs'])
+      } for connector in _get_installed_connectors(categories=['editor', 'catalogs'], user=user)
     ]
     ]
-    # No connector permission currently
   else:
   else:
     if not INTERPRETERS.get():
     if not INTERPRETERS.get():
       _default_interpreters(user)
       _default_interpreters(user)
@@ -69,7 +68,7 @@ def get_ordered_interpreters(user=None):
     user_interpreters = []
     user_interpreters = []
     for interpreter in interpreters:
     for interpreter in interpreters:
       if check_permissions(user, interpreter, user_apps=user_apps):
       if check_permissions(user, interpreter, user_apps=user_apps):
-        pass # Not allowed
+        pass  # Not allowed
       else:
       else:
         user_interpreters.append(interpreter)
         user_interpreters.append(interpreter)