This is an automated email from the ASF dual-hosted git repository.

stoty pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/phoenix-queryserver.git


The following commit(s) were added to refs/heads/master by this push:
     new 12fe6c7  PHOENIX-5938 Support impersonation in the python driver
12fe6c7 is described below

commit 12fe6c7c56f078427d375c7fcfd5547074745020
Author: Istvan Toth <st...@apache.org>
AuthorDate: Fri Jun 5 16:51:30 2020 +0200

    PHOENIX-5938 Support impersonation in the python driver
    
    also minor authentication fixes and improvements
---
 python/phoenixdb/phoenixdb/__init__.py             | 77 ++++++++++++++++------
 python/phoenixdb/phoenixdb/avatica/client.py       |  5 +-
 python/phoenixdb/phoenixdb/sqlalchemy_phoenix.py   | 23 +++++--
 python/phoenixdb/phoenixdb/tests/test_avatica.py   | 25 +++++++
 .../phoenixdb/phoenixdb/tests/test_sqlalchemy.py   |  7 +-
 5 files changed, 107 insertions(+), 30 deletions(-)

diff --git a/python/phoenixdb/phoenixdb/__init__.py 
b/python/phoenixdb/phoenixdb/__init__.py
index 4f5378e..14e5a8d 100644
--- a/python/phoenixdb/phoenixdb/__init__.py
+++ b/python/phoenixdb/phoenixdb/__init__.py
@@ -54,7 +54,7 @@ For example::
 
 
 def connect(url, max_retries=None, auth=None, authentication=None, 
avatica_user=None, avatica_password=None,
-            truststore=None, verify=None, **kwargs):
+            truststore=None, verify=None, do_as=None, user=None, 
password=None, **kwargs):
     """Connects to a Phoenix query server.
 
     :param url:
@@ -77,6 +77,14 @@ def connect(url, max_retries=None, auth=None, 
authentication=None, avatica_user=
         Authentication configuration object as expected by the underlying 
python_requests and
         python_requests_gssapi library
 
+    :param verify:
+        The path to the PEM file for verifying the server's certificate. It is 
passed directly to
+        the `~verify` parameter of the underlying python_requests library.
+        Setting it to False disables the server certificate verification.
+
+    :param do_as:
+        Username to impersonate (sets the Hadoop doAs URL parameter)
+
     :param authentication:
         Alternative way to specify the authentication mechanism that mimics
         the semantics of the JDBC drirver
@@ -89,10 +97,12 @@ def connect(url, max_retries=None, auth=None, 
authentication=None, avatica_user=
         Password for BASIC or DIGEST authentication. Use in conjunction with 
the
         `~authentication' option.
 
-    :param verify:
-        The path to the PEM file for verifying the server's certificate. It is 
passed directly to
-        the `~verify` parameter of the underlying python_requests library.
-        Setting it to false disables the server certificate verification.
+    :param user
+        If `~authentication' is BASIC or DIGEST then alias for `~avatica_user`
+        If `~authentication' is NONE or SPNEGO then alias for `~do_as`
+
+    :param password
+        If `~authentication' is BASIC or DIGEST then is alias for 
`~avatica_password`
 
     :param truststore:
         Alias for verify
@@ -101,33 +111,65 @@ def connect(url, max_retries=None, auth=None, 
authentication=None, avatica_user=
         :class:`~phoenixdb.connection.Connection` object.
     """
 
+    (url, auth, verify) = _process_args(
+        url, auth=auth, authentication=authentication,
+        avatica_user=avatica_user, avatica_password=avatica_password,
+        truststore=truststore, verify=verify, do_as=do_as, user=user, 
password=password)
+
+    client = AvaticaClient(url, max_retries=max_retries, auth=auth, 
verify=verify)
+    client.connect()
+    return Connection(client, **kwargs)
+
+
+def _process_args(
+        url, auth=None, authentication=None, avatica_user=None, 
avatica_password=None,
+        truststore=None, verify=None, do_as=None, user=None, password=None):
     url_parsed = urlparse(url)
     url_params = parse_qs(url_parsed.query, keep_blank_values=True)
 
-    # Parse supported JDBC compatible options from URL. args have precendece
-    rebuild = False
+    # Parse supported JDBC compatible parameters from URL. args have precendece
+    # Unlike the JDBC driver, we are expecting these as query params, as the 
avatica java client
+    # has a different idea of what an URL param is than urlparse. (urlparse 
seems just broken
+    # in this regard)
+    params_changed = False
     if auth is None and authentication is None and 'authentication' in 
url_params:
         authentication = url_params['authentication'][0]
         del url_params['authentication']
-        rebuild = True
+        params_changed = True
 
     if avatica_user is None and 'avatica_user' in url_params:
         avatica_user = url_params['avatica_user'][0]
         del url_params['avatica_user']
-        rebuild = True
+        params_changed = True
 
     if avatica_password is None and 'avatica_password' in url_params:
         avatica_password = url_params['avatica_password'][0]
         del url_params['avatica_password']
-        rebuild = True
+        params_changed = True
 
     if verify is None and truststore is None and 'truststore' in url_params:
         truststore = url_params['truststore'][0]
         del url_params['truststore']
-        rebuild = True
-
-    if rebuild:
-        url_parsed._replace(query=urlencode(url_params, True))
+        params_changed = True
+
+    if authentication == 'BASIC' or authentication == 'DIGEST':
+        # Handle standard user and password parameters
+        if user is not None and avatica_user is None:
+            avatica_user = user
+        if password is not None and avatica_password is None:
+            avatica_password = password
+    else:
+        # interpret standard user parameter as do_as for SPNEGO and NONE
+        if user is not None and do_as is None:
+            do_as = user
+
+    # Add doAs
+    if do_as:
+        url_params['doAs'] = do_as
+        params_changed = True
+
+    if params_changed:
+        url_parsed = url_parsed._replace(query=urlencode(url_params))
         url = urlunparse(url_parsed)
 
     if auth == "SPNEGO":
@@ -144,9 +186,4 @@ def connect(url, max_retries=None, auth=None, 
authentication=None, avatica_user=
     if verify is None and truststore is not None:
         verify = truststore
 
-    client = AvaticaClient(url, max_retries=max_retries,
-                           auth=auth,
-                           verify=verify
-                           )
-    client.connect()
-    return Connection(client, **kwargs)
+    return (url, auth, verify)
diff --git a/python/phoenixdb/phoenixdb/avatica/client.py 
b/python/phoenixdb/phoenixdb/avatica/client.py
index daad12e..fdbe6bb 100644
--- a/python/phoenixdb/phoenixdb/avatica/client.py
+++ b/python/phoenixdb/phoenixdb/avatica/client.py
@@ -92,7 +92,10 @@ OPEN_CONNECTION_PROPERTIES = (
     'auth',
     'authentication',
     'truststore',
-    'verify'
+    'verify',
+    'do_as',
+    'user',
+    'password'
 )
 
 
diff --git a/python/phoenixdb/phoenixdb/sqlalchemy_phoenix.py 
b/python/phoenixdb/phoenixdb/sqlalchemy_phoenix.py
index e109fff..ebe8f12 100644
--- a/python/phoenixdb/phoenixdb/sqlalchemy_phoenix.py
+++ b/python/phoenixdb/phoenixdb/sqlalchemy_phoenix.py
@@ -12,6 +12,11 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import re
+import sys
+
+import phoenixdb
+
 from sqlalchemy import types
 from sqlalchemy.engine.default import DefaultDialect, DefaultExecutionContext
 from sqlalchemy.exc import CompileError
@@ -19,10 +24,6 @@ from sqlalchemy.sql.compiler import DDLCompiler
 from sqlalchemy.types import BIGINT, BOOLEAN, CHAR, DATE, DECIMAL, FLOAT, 
INTEGER, NUMERIC,\
     SMALLINT, TIME, TIMESTAMP, VARBINARY, VARCHAR
 
-import phoenixdb
-import re
-import sys
-
 if sys.version_info.major == 3:
     from urllib.parse import urlunsplit, SplitResult, urlencode
 else:
@@ -94,6 +95,13 @@ class PhoenixDialect(DefaultDialect):
     execution_ctx_cls = PhoenixExecutionContext
 
     def __init__(self, tls=False, path='/', **opts):
+        '''
+        :param tls:
+            If True, then use https for connecting, otherwise use http
+
+        :param path:
+            The path component of the connection URL
+        '''
         # There is no way to pass these via the SqlAlchemy url object
         self.tls = tls
         self.path = path
@@ -104,6 +112,11 @@ class PhoenixDialect(DefaultDialect):
         return phoenixdb
 
     def create_connect_args(self, url):
+        connect_args = dict()
+        if url.username is not None:
+            connect_args['user'] = url.username
+            if url.password is not None:
+                connect_args['password'] = url.username
         phoenix_url = urlunsplit(SplitResult(
             scheme='https' if self.tls else 'http',
             netloc='{}:{}'.format(url.host, 8765 if url.port is None else 
url.port),
@@ -111,7 +124,7 @@ class PhoenixDialect(DefaultDialect):
             query=urlencode(url.query),
             fragment='',
         ))
-        return [phoenix_url], {}
+        return [phoenix_url], connect_args
 
     def has_table(self, connection, table_name, schema=None):
         if schema is None:
diff --git a/python/phoenixdb/phoenixdb/tests/test_avatica.py 
b/python/phoenixdb/phoenixdb/tests/test_avatica.py
index 20a7e0b..04724a6 100644
--- a/python/phoenixdb/phoenixdb/tests/test_avatica.py
+++ b/python/phoenixdb/phoenixdb/tests/test_avatica.py
@@ -15,8 +15,11 @@
 
 import unittest
 
+import phoenixdb
 from phoenixdb.avatica.client import parse_url, urlparse
 
+from requests.auth import HTTPBasicAuth
+
 
 class ParseUrlTest(unittest.TestCase):
 
@@ -24,3 +27,25 @@ class ParseUrlTest(unittest.TestCase):
         self.assertEqual(urlparse.urlparse('http://localhost:8765/'), 
parse_url('localhost'))
         self.assertEqual(urlparse.urlparse('http://localhost:2222/'), 
parse_url('localhost:2222'))
         self.assertEqual(urlparse.urlparse('http://localhost:2222/'), 
parse_url('http://localhost:2222/'))
+
+    def test_url_params(self):
+        (url, auth, verify) = phoenixdb._process_args((
+            "https://localhost:8765?authentication=BASIC&";
+            
"avatica_user=user&avatica_password=password&truststore=truststore"))
+        self.assertEqual("https://localhost:8765";, url)
+        self.assertEqual("truststore", verify)
+        self.assertEqual(auth, HTTPBasicAuth("user", "password"))
+
+        (url, auth, verify) = phoenixdb._process_args(
+            "http://localhost:8765";, authentication='BASIC', user='user', 
password='password',
+            truststore='truststore')
+        self.assertEqual("http://localhost:8765";, url)
+        self.assertEqual("truststore", verify)
+        self.assertEqual(auth, HTTPBasicAuth("user", "password"))
+
+        (url, auth, verify) = phoenixdb._process_args(
+            "https://localhost:8765";, authentication='SPNEGO', user='user', 
truststore='truststore')
+        self.assertEqual("https://localhost:8765?doAs=user";, url)
+        self.assertEqual("truststore", verify)
+        # SPNEGO auth objects seem to have no working __eq__
+        # self.assertEqual(auth, HTTPSPNEGOAuth(opportunistic_auth=True))
diff --git a/python/phoenixdb/phoenixdb/tests/test_sqlalchemy.py 
b/python/phoenixdb/phoenixdb/tests/test_sqlalchemy.py
index fe7bd1d..52bff73 100644
--- a/python/phoenixdb/phoenixdb/tests/test_sqlalchemy.py
+++ b/python/phoenixdb/phoenixdb/tests/test_sqlalchemy.py
@@ -13,14 +13,14 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import unittest
 import sys
+import unittest
 
 import sqlalchemy as db
 from sqlalchemy import text
 
-from . import TEST_DB_URL, TEST_DB_AUTHENTICATION, TEST_DB_AVATICA_USER, 
TEST_DB_AVATICA_PASSWORD,\
-        TEST_DB_TRUSTSTORE
+from . import TEST_DB_AUTHENTICATION, TEST_DB_AVATICA_PASSWORD, 
TEST_DB_AVATICA_USER, \
+    TEST_DB_TRUSTSTORE, TEST_DB_URL
 
 if sys.version_info.major == 3:
     from urllib.parse import urlparse, urlunparse
@@ -67,7 +67,6 @@ class SQLAlchemyTest(unittest.TestCase):
                 CONSTRAINT my_pk PRIMARY KEY (state, city))'''))
                 columns_result = inspector.get_columns('us_population')
                 self.assertEqual(len(columns_result), 3)
-                print(columns_result)
             finally:
                 connection.execute('drop table if exists us_population')
 

Reply via email to