Przeglądaj źródła

[dbms] Support asking for JDBC credentials

If 'user' and 'password' are omitted, they will be prompted in the UI.
options='{"url": "jdbc:mysql://localhost:3306/hue", "driver": "com.mysql.jdbc.Driver", "user": "root", "password": "root"}'

On new session we don't automatically execute the snippet after the aut. On session expiration we do.
Romain Rigaux 10 lat temu
rodzic
commit
64da5fc

+ 1 - 1
apps/search/src/search/static/search/js/search.ko.js

@@ -1601,7 +1601,7 @@ var SearchViewModel = function (collection_json, query_json, initial_json) {
       }
 
       analyse.update();
-      $(document).trigger("shownAnalysis")
+      $(document).trigger("shownAuthModal");
     }
   }
 

+ 1 - 0
desktop/conf.dist/hue.ini

@@ -581,6 +581,7 @@
     #   name=MySql JDBC
     #   interface=jdbc
     #   ## Specific options for connecting to the server.
+    #   ## If 'user' and 'password' are omitted, they will be prompted in the UI.
     #   options='{"url": "jdbc:mysql://localhost:3306/hue", "driver": "com.mysql.jdbc.Driver", "user": "root", "password": "root"}'
 
 

+ 1 - 0
desktop/conf/pseudo-distributed.ini.tmpl

@@ -585,6 +585,7 @@
     #   name=MySql JDBC
     #   interface=jdbc
     #   ## Specific options for connecting to the server.
+    #   ## If 'user' and 'password' are omitted, they will be prompted in the UI.
     #   options='{"url": "jdbc:mysql://localhost:3306/hue", "driver": "com.mysql.jdbc.Driver", "user": "root", "password": "root"}'
 
 

+ 0 - 1
desktop/libs/notebook/src/notebook/api.py

@@ -51,7 +51,6 @@ def create_session(request):
       properties = old_session[0]['properties']
 
   response['session'] = get_api(request.user, session, request.fs, request.jt).create_session(lang=session['type'], properties=properties)
-  response['session']['properties'] = properties
   response['status'] = 0
 
   return JsonResponse(response)

+ 4 - 1
desktop/libs/notebook/src/notebook/connectors/base.py

@@ -36,6 +36,9 @@ class SessionExpired(Exception):
 class QueryExpired(Exception):
   pass
 
+class AuthenticationRequired(Exception):
+  pass
+
 
 class QueryError(Exception):
   def __init__(self, message):
@@ -129,7 +132,7 @@ class Api(object):
     return {
         'type': lang,
         'id': None,
-        'properties': []
+        'properties': properties if not None else []
     }
 
   def close_session(self, session):

+ 34 - 2
desktop/libs/notebook/src/notebook/connectors/jdbc.py

@@ -23,16 +23,22 @@ from desktop.lib.exceptions_renderable import PopupException
 from desktop.lib.i18n import force_unicode
 from librdbms.jdbc import Jdbc, query_and_fetch
 
-from notebook.connectors.base import Api, QueryError
+from notebook.connectors.base import Api, QueryError, AuthenticationRequired
 
 
 LOG = logging.getLogger(__name__)
 
 
+# Cache one JDBC connection by user for not saving user credentials
+API_CACHE = {}
+
+
 def query_error_handler(func):
   def decorator(*args, **kwargs):
     try:
       return func(*args, **kwargs)
+    except AuthenticationRequired, e:
+      raise e
     except Exception, e:
       message = force_unicode(str(e))
       if 'error occurred while trying to connect to the Java server' in message:
@@ -45,12 +51,38 @@ def query_error_handler(func):
 class JdbcApi(Api):
 
   def __init__(self, user, fs=None, jt=None, options=None):
+    global API_CACHE
     Api.__init__(self, user, fs=fs, jt=jt, options=options)
 
-    self.db = Jdbc(self.options['driver'], self.options['url'], self.options['user'], self.options['password'])
+    self.db = None
+
+    if self.options['user'] in API_CACHE:
+      self.db = API_CACHE[self.options['user']]
+    elif 'password' in self.options:
+      self.db = API_CACHE[self.options['user']] = Jdbc(self.options['driver'], self.options['url'], self.options['user'], self.options['password'])
+
+  def create_session(self, lang=None, properties=None):
+    props = super(JdbcApi, self).create_session(lang, properties)
+
+    properties = dict([(p['name'], p['value']) for p in properties]) if properties is not None else {}
+    props['properties'] = {} # We don't store passwords
+
+    if self.db is None:
+      if 'password' in properties:
+        user = properties.get('user') or self.options.get('user')
+        props['properties'] = {'user': user}
+        self.db = API_CACHE[user] = Jdbc(self.options['driver'], self.options['url'], user, properties.pop('password'))
+
+    if self.db is None:
+      raise AuthenticationRequired()
+
+    return props
 
   @query_error_handler
   def execute(self, notebook, snippet):
+    if self.db is None:
+      raise AuthenticationRequired()
+
     data, description = query_and_fetch(self.db, snippet['statement'], 100)
     has_result_set = data is not None
 

+ 0 - 6
desktop/libs/notebook/src/notebook/connectors/spark_batch.py

@@ -27,12 +27,6 @@ LOG = logging.getLogger(__name__)
 
 class SparkBatchApi(Api):
 
-  def create_session(self, lang, properties=None):
-    return {
-        'type': lang,
-        'id': None
-    }
-
   def execute(self, notebook, snippet):
     api = get_spark_api(self.user)
 

+ 4 - 4
desktop/libs/notebook/src/notebook/connectors/spark_shell.py

@@ -57,13 +57,13 @@ class SparkApi(Api):
   STANDALONE_JOB_RE = re.compile("Got job (\d+)")
 
   def create_session(self, lang='scala', properties=None):
-    properties = dict([(p['name'], p['value']) for p in properties]) if properties is not None else {}
+    props = dict([(p['name'], p['value']) for p in properties]) if properties is not None else {}
 
-    properties['kind'] = lang
+    props['kind'] = lang
 
     api = get_spark_api(self.user)
 
-    response = api.create_session(**properties)
+    response = api.create_session(**props)
 
     status = api.get_session(response['id'])
     count = 0
@@ -80,7 +80,7 @@ class SparkApi(Api):
     return {
         'type': lang,
         'id': response['id'],
-        'properties': []
+        'properties': properties
     }
 
   def execute(self, notebook, snippet):

+ 5 - 3
desktop/libs/notebook/src/notebook/decorators.py

@@ -27,7 +27,7 @@ from desktop.lib.exceptions_renderable import PopupException
 from desktop.lib.i18n import force_unicode
 from desktop.models import Document2, Document
 
-from notebook.connectors.base import QueryExpired, QueryError, SessionExpired
+from notebook.connectors.base import QueryExpired, QueryError, SessionExpired, AuthenticationRequired
 
 
 LOG = logging.getLogger(__name__)
@@ -72,13 +72,15 @@ def check_document_modify_permission():
 def api_error_handler(func):
   def decorator(*args, **kwargs):
     response = {}
-    
+
     try:
       return func(*args, **kwargs)
     except SessionExpired, e:
-      response['status'] = -2    
+      response['status'] = -2
     except QueryExpired, e:
       response['status'] = -3
+    except AuthenticationRequired, e:
+      response['status'] = 403
     except QueryError, e:
       LOG.exception('error running %s' % func)
       response['status'] = 1

+ 26 - 3
desktop/libs/notebook/src/notebook/static/notebook/js/notebook.ko.js

@@ -365,6 +365,10 @@ var Snippet = function (vm, notebook, snippet) {
     else if (data.status == -3) { // Statement expired
       self.status('expired');
     }
+    else if (data.status == 403) { // Auth required
+      self.status('expired');
+      $(document).trigger("showAuthModal", {'type': self.type(), 'snippet': self});
+    }
     else if (data.status == 1 || data.status == -1) {
       self.status('failed');
       var match = ERROR_REGEX.exec(data.message);
@@ -381,7 +385,7 @@ var Snippet = function (vm, notebook, snippet) {
   self.lastExecuted = 0;
 
   self.execute = function () {
-    var now = (new Date()).getTime(); // we don't allow fast clicks
+    var now = (new Date()).getTime(); // We don't allow fast clicks
     if (self.status() == 'running' || self.status() == 'loading' || now - self.lastExecuted < 1000) {
       return;
     }
@@ -487,7 +491,7 @@ var Snippet = function (vm, notebook, snippet) {
       notebook.newSnippet();
     }
   };
-  
+
   self.fetchResultMetadata = function () {
     $.post("/notebook/api/fetch_result_metadata", {
       notebook: ko.mapping.toJSON(notebook.getContext()),
@@ -769,7 +773,7 @@ var Notebook = function (vm, notebook) {
 
     $.post("/notebook/api/create_session", {
       notebook: ko.mapping.toJSON(self.getContext()),
-      session: ko.mapping.toJSON(session) // e.g. {'type': 'hive', 'properties': [{'driverCores': '2'}]}
+      session: ko.mapping.toJSON(session) // e.g. {'type': 'hive', 'properties': [{'name': driverCores', 'value', '2'}]}
     }, function (data) {
       if (data.status == 0) {
         ko.mapping.fromJS(data.session, {}, session);
@@ -782,6 +786,8 @@ var Notebook = function (vm, notebook) {
         if (callback) {
           setTimeout(callback, 500);
         }
+      } else if (data.status == 403) {
+        $(document).trigger("showAuthModal", {'type': session.type()});
       }
       else {
         fail(data.message);
@@ -793,6 +799,18 @@ var Notebook = function (vm, notebook) {
     })
   };
 
+  self.authSession = function () {
+    self.createSession(new Session(vm, {
+          'type': vm.authSessionType(),
+          'properties': [
+            {'name': 'user', 'value': vm.authSessionUsername()},
+            {'name': 'password', 'value': vm.authSessionPassword()}
+          ]
+        }),
+        vm.authSessionSnippet() ? vm.authSessionSnippet().execute : null  // On new session we don't automatically execute the snippet after the aut. On session expiration we do.
+    );
+  };
+
   self.newSnippet = function (type) {
     if (type) {
       self.selectedSnippet(type);
@@ -921,6 +939,11 @@ function EditorViewModel(notebooks, options) {
     self.isEditing(!self.isEditing());
   };
 
+  self.authSessionUsername = ko.observable(); // UI popup
+  self.authSessionPassword = ko.observable();
+  self.authSessionType = ko.observable();
+  self.authSessionSnippet = ko.observable();
+
   self.removeSnippetConfirmation = ko.observable();
 
   self.removeSnippet = function (notebook, snippet) {

+ 24 - 0
desktop/libs/notebook/src/notebook/templates/editor_components.mako

@@ -931,6 +931,22 @@ from desktop.views import _ko
   <p class="hoverText">${_('Drop iPython/Zeppelin notebooks here')}</p>
 </div>
 
+
+<div id="authModal" class="modal hide fade">
+  <div class="modal-header">
+    <a href="#" class="close" data-dismiss="modal">&times;</a>
+    <h3>${_('Connect to the data source')}</h3>
+  </div>
+  <div class="modal-body">
+    <input name="username" type="text" data-bind="value: $root.authSessionUsername" placeholder="${ _('Username') }"/>
+    <input name="password" type="password" data-bind="value: $root.authSessionPassword" placeholder="${ _('Password') }"/>
+  </div>
+  <div class="modal-footer">
+    <a class="btn" data-dismiss="modal">${_('No')}</a>
+    <input type="submit" data-dismiss="modal" value="${_('Yes')}" class="btn btn-danger" data-bind="click: function() { $root.selectedNotebook().authSession(); }" />
+  </div>
+</div>
+
 </%def>
 
 
@@ -1837,5 +1853,13 @@ from desktop.views import _ko
       $(".main-content").css("top", "70px");
     }
   });
+
+  $(document).on("showAuthModal", function(e, data) {
+    viewModel.authSessionUsername('${ user.username }');
+    viewModel.authSessionPassword('');
+    viewModel.authSessionType(data['type']);
+    viewModel.authSessionSnippet(data['snippet']);
+    $("#authModal").modal("show");
+  });
 </script>
 </%def>

+ 1 - 1
tools/scripts/hue-review

@@ -1,4 +1,4 @@
-#!/bin/sh
+#!/usr/bin/env bash
 
 if [ $# -lt 3 ]; then
 	echo "Usage: hue-review rev-list reviewer(s) summary ..." 1>&2